From 18a079dad5c33ecc270550fe556d847d89669f99 Mon Sep 17 00:00:00 2001 From: =?utf8?q?F=C3=A9lix=20Sipma?= Date: Mon, 23 Jan 2017 14:20:49 +0000 Subject: [PATCH] Import patat_0.4.7.1.orig.tar.gz [dgit import orig patat_0.4.7.1.orig.tar.gz] --- .gitignore | 7 + .travis.yml | 11 + CHANGELOG.md | 61 +++ LICENSE | 339 ++++++++++++++++ Makefile | 30 ++ README.md | 365 ++++++++++++++++++ Setup.hs | 2 + extra/make-man.hs | 110 ++++++ extra/screenshot.png | Bin 0 -> 52076 bytes patat.cabal | 60 +++ src/Data/Aeson/Extended.hs | 22 ++ src/Data/Aeson/TH/Extended.hs | 21 + src/Data/Data/Extended.hs | 23 ++ src/Main.hs | 181 +++++++++ src/Patat/AutoAdvance.hs | 52 +++ src/Patat/Presentation.hs | 20 + src/Patat/Presentation/Display.hs | 313 +++++++++++++++ src/Patat/Presentation/Display/CodeBlock.hs | 79 ++++ src/Patat/Presentation/Display/Table.hs | 107 ++++++ src/Patat/Presentation/Fragment.hs | 134 +++++++ src/Patat/Presentation/Interactive.hs | 122 ++++++ src/Patat/Presentation/Internal.hs | 107 ++++++ src/Patat/Presentation/Read.hs | 156 ++++++++ src/Patat/PrettyPrint.hs | 404 ++++++++++++++++++++ src/Patat/Theme.hs | 286 ++++++++++++++ src/Text/Pandoc/Extended.hs | 30 ++ stack.yaml | 6 + test.sh | 30 ++ tests/01.md | 14 + tests/01.md.dump | 8 + tests/02.lhs | 6 + tests/02.lhs.dump | 8 + tests/03.md | 46 +++ tests/03.md.dump | 48 +++ tests/deflist.md | 20 + tests/deflist.md.dump | 24 ++ tests/fragments.md | 27 ++ tests/fragments.md.dump | 54 +++ tests/links.md | 8 + tests/links.md.dump | 10 + tests/lists.md | 13 + tests/lists.md.dump | 15 + tests/meta.md | 12 + tests/meta.md.dump | 7 + tests/syntax.md | 14 + tests/syntax.md.dump | 7 + tests/tables.md | 48 +++ tests/tables.md.dump | 48 +++ tests/themes.md | 11 + tests/themes.md.dump | 5 + tests/wrapping.md | 23 ++ tests/wrapping.md.dump | 17 + 52 files changed, 3571 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 Setup.hs create mode 100644 extra/make-man.hs create mode 100644 extra/screenshot.png create mode 100644 patat.cabal create mode 100644 src/Data/Aeson/Extended.hs create mode 100644 src/Data/Aeson/TH/Extended.hs create mode 100644 src/Data/Data/Extended.hs create mode 100644 src/Main.hs create mode 100644 src/Patat/AutoAdvance.hs create mode 100644 src/Patat/Presentation.hs create mode 100644 src/Patat/Presentation/Display.hs create mode 100644 src/Patat/Presentation/Display/CodeBlock.hs create mode 100644 src/Patat/Presentation/Display/Table.hs create mode 100644 src/Patat/Presentation/Fragment.hs create mode 100644 src/Patat/Presentation/Interactive.hs create mode 100644 src/Patat/Presentation/Internal.hs create mode 100644 src/Patat/Presentation/Read.hs create mode 100644 src/Patat/PrettyPrint.hs create mode 100644 src/Patat/Theme.hs create mode 100644 src/Text/Pandoc/Extended.hs create mode 100644 stack.yaml create mode 100644 test.sh create mode 100644 tests/01.md create mode 100644 tests/01.md.dump create mode 100644 tests/02.lhs create mode 100644 tests/02.lhs.dump create mode 100644 tests/03.md create mode 100644 tests/03.md.dump create mode 100644 tests/deflist.md create mode 100644 tests/deflist.md.dump create mode 100644 tests/fragments.md create mode 100644 tests/fragments.md.dump create mode 100644 tests/links.md create mode 100644 tests/links.md.dump create mode 100644 tests/lists.md create mode 100644 tests/lists.md.dump create mode 100644 tests/meta.md create mode 100644 tests/meta.md.dump create mode 100644 tests/syntax.md create mode 100644 tests/syntax.md.dump create mode 100644 tests/tables.md create mode 100644 tests/tables.md.dump create mode 100644 tests/themes.md create mode 100644 tests/themes.md.dump create mode 100644 tests/wrapping.md create mode 100644 tests/wrapping.md.dump diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da4d999 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.o +*.hi +extra/make-man +extra/patat.1 +.stack-work +dist +tags diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7759079 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: haskell +ghc: '7.8' +sudo: false +cache: + directories: + - '$HOME/.cabal' + - '$HOME/.ghc' +install: + - cabal install +script: + - make test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ea6b6b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +- 0.4.7.1 (2017-01-22) + * Bump `directory-1.3` dependency + * Bump `time-1.7` dependency + +- 0.4.7.0 (2017-01-20) + * Bump `aeson-1.1` dependency + * Parse YAML for settings using `yaml` instead of pandoc + * Clarify watch & autoAdvance combination in documentation. + +- 0.4.6.0 (2016-12-28) + * Redraw the screen on unknown commands to prevent accidental typing from + showing up. + * Make the cursor invisible during the presentation. + * Move the footer down one more line to gain some screen real estate. + +- 0.4.5.0 (2016-12-05) + * Render the date in a locale-independent manner (patch by Daniel + Shahaf). + +- 0.4.4.0 (2016-12-03) + * Force the use of UTF-8 when generating the man page. + +- 0.4.3.0 (2016-12-02) + * Use `SOURCE_DATE_EPOCH` if it is present instead of getting the date from + `git log`. + +- 0.4.2.0 (2016-12-01) + * Fix issues with man page generation on Travis. + +- 0.4.1.0 (2016-12-01) + * Fix compatibility with `pandoc-1.18` and `pandoc-1.19`. + * Add a man page. + +- 0.4.0.0 (2016-11-15) + * Add configurable auto advancing. + * Support fragmented slides. + +- 0.3.3.0 (2016-10-31) + * Add a `--version` flag. + * Add support for `pandoc-1.18` which includes a new `LineBlock` element. + +- 0.3.2.0 (2016-10-20) + * Keep running even if errors are encountered during reload. + +- 0.3.1.0 (2016-10-18) + * Fix compilation with `lts-6.22`. + +- 0.3.0.0 (2016-10-17) + * Add syntax highlighting support. + * Fixed slide clipping after reload. + +- 0.2.0.0 (2016-10-13) + * Add theming support. + * Fix links display. + * Add support for wrapping. + * Allow org mode as input format. + +- 0.1.0.0 (2016-10-02) + * Upload first version from hotel wifi in Kalaw. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f53f40 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68c36b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +# The minor version is passed to the build. This is used to do some CPP to +# solve incompatibilities. +PANDOC_MINOR_VERSION=$(shell ghc-pkg latest pandoc | sed 's/.*-//' | cut -d. -f2) + +# We use `?=` to set SOURCE_DATE_EPOCH only if it is not present. Unfortunately +# we can't use `git --date=unix` since only very recent git versions support +# that, so we need to make a round trip through `date`. +SOURCE_DATE_EPOCH?=$(shell date '+%s' \ + --date="$(shell git log -1 --format=%cd --date=rfc)") + +# Prettify the date. +SOURCE_DATE=$(shell env LC_ALL=C date '+%B %d, %Y' -d "@${SOURCE_DATE_EPOCH}") + +extra/patat.1: README.md extra/make-man + SOURCE_DATE="$(SOURCE_DATE)" ./extra/make-man >$@ + +extra/make-man: extra/make-man.hs + ghc -DPANDOC_MINOR_VERSION=${PANDOC_MINOR_VERSION} -Wall -o $@ $< + +man: extra/patat.1 + +# Also check if we can generate the manual. +test: man + bash test.sh + +clean: + rm -f extra/patat.1 + rm -f extra/make-man + +.PHONY: man test clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4d52a2 --- /dev/null +++ b/README.md @@ -0,0 +1,365 @@ +patat +===== + +[![Build Status](https://img.shields.io/travis/jaspervdj/patat.svg)](https://travis-ci.org/jaspervdj/patat) [![Hackage](https://img.shields.io/hackage/v/patat.svg)](https://hackage.haskell.org/package/patat) [![GitHub tag](https://img.shields.io/github/tag/jaspervdj/patat.svg)]() + +`patat` (**P**resentations **A**top **T**he **A**NSI **T**erminal) is a small +tool that allows you to show presentations using only an ANSI terminal. It does +not require `ncurses`. + +Features: + +- Leverages the great [Pandoc] library to support many input formats including + [Literate Haskell]. +- Supports [smart slide splitting](#input-format). +- Slides can be split up into [multiple fragments](#fragmented-slides) +- There is a [live reload](#running) mode. +- [Theming](#theming) support. +- [Auto advancing](#auto-advancing) with configurable delay. +- Optionally [re-wrapping](#line-wrapping) text to terminal width with proper + indentation. +- Syntax highlighting for nearly one hundred languages generated from [Kate] + syntax files. +- Written in [Haskell]. + +![screenshot](extra/screenshot.png?raw=true) + +[Kate]: https://kate-editor.org/ +[Haskell]: http://haskell.org/ +[Pandoc]: http://pandoc.org/ + +Table of Contents +----------------- + +- [Table of Contents](#table-of-contents) +- [Installation](#installation) + - [Pre-built-packages](#pre-built-packages) + - [From source](#from-source) +- [Running](#running) +- [Options](#options) +- [Controls](#controls) +- [Input format](#input-format) +- [Configuration](#configuration) + - [Line wrapping](#line-wrapping) + - [Auto advancing](#auto-advancing) + - [Fragmented slides](#fragmented-slides) + - [Theming](#theming) + - [Syntax Highlighting](#syntax-highlighting) +- [Trivia](#trivia) + +Installation +------------ + +### Pre-built-packages + +There is a pre-built package available for Debian: + +- + +### From source + +Installation from source is very easy. You can build from source using `stack +install` or `cabal install`. `patat` is also available from [Hackage]. + +[Hackage]: https://hackage.haskell.org/package/patat + +For people unfamiliar with the Haskell ecosystem, this means you can do either +of the following: + +#### Using stack + +1. Install [stack] for your platform. +2. Clone this repository. +3. Run `stack setup` (if you're running stack for the first time) and + `stack install`. +4. Make sure `$HOME/.local/bin` is in your `$PATH`. + +[stack]: https://docs.haskellstack.org/en/stable/README/ + +#### Using cabal + +1. Install [cabal] for your platform. +2. Run `cabal install patat`. +3. Make sure `$HOME/.cabal/bin` is in your `$PATH`. + +[cabal]: https://www.haskell.org/cabal/ + +Running +------- + +`patat [*options*] file` + +Options +------- + +`-w`, `--watch` + +: If you provide the `--watch` flag, `patat` will watch the presentation file + for changes and reload automatically. This is very useful when you are + writing the presentation. + +`-f`, `--force` + +: Run the presentation even if the terminal claims it does not support ANSI + features. + +`-d`, `--dump` + +: Just dump all the slides to stdout. This is useful for debugging. + +`--version` + +: Display version information. + +Controls +-------- + +- **Next slide**: `space`, `enter`, `l`, `→` +- **Previous slide**: `backspace`, `h`, `←` +- **Go forward 10 slides**: `j`, `↓` +- **Go backward 10 slides**: `k`, `↑` +- **First slide**: `0` +- **Last slide**: `G` +- **Reload file**: `r` +- **Quit**: `q` + +The `r` key is very useful since it allows you to preview your slides while you +are writing them. You can also use this to fix artifacts when the terminal is +resized. + +Input format +------------ + +The input format can be anything that Pandoc supports. Plain markdown is +usually the most simple solution: + + --- + title: This is my presentation + author: Jane Doe + ... + + # This is a slide + + Slide contents. Yay. + + --- + + # Important title + + Things I like: + + - Markdown + - Haskell + - Pandoc + +Horizontal rulers (`---`) are used to split slides. + +However, if you prefer not use these since they are a bit intrusive in the +markdown, you can also start every slide with an `h1` header. In that case, the +file should not contain a single horizontal ruler. + +This means the following document is equivalent: + + --- + title: This is my presentation + author: Jane Doe + ... + + # This is a slide + + Slide contents. Yay. + + # Important title + + Things I like: + + - Markdown + - Haskell + - Pandoc + +Configuration +------------- + +`patat` is fairly configurable. The configuration is done using [YAML]. There +are two places where you can put your configuration: + +1. In the presentation file itself, using the [Pandoc metadata header]. +2. In `$HOME/.patat.yaml` + +[YAML]: http://yaml.org/ +[Pandoc metadata header]: http://pandoc.org/MANUAL.html#extension-yaml_metadata_block + +For example, we set an option `key` to `val` by using the following file: + + --- + title: Presentation with options + author: John Doe + patat: + key: val + ... + + Hello world. + +Or we can use a normal presentation and have the following `$HOME/.patat.yaml`: + + key: val + +### Line wrapping + +Line wrapping can be enabled by setting `wrap: true` in the configuration. This +will re-wrap all lines to fit the terminal width better. + +### Auto advancing + +By setting `autoAdvanceDelay` to a number of seconds, `patat` will automatically +advance to the next slide. + + --- + title: Auto-advance, yes please + author: John Doe + patat: + autoAdvanceDelay: 2 + ... + + Hello World! + + --- + + This slide will be shown two seconds after the presentation starts. + +Note that changes to `autoAdvanceDelay` are not picked up automatically if you +are running `patat --watch`. This requires restarting `patat`. + +### Fragmented slides + +By default, slides are always displayed "all at once". If you want to display +them fragment by fragment, there are two ways to do that. The most common +case is that lists should be displayed incrementally. + +This can be configured by settings `incrementalLists` to `true` in the metadata +block: + + --- + title: Presentation with incremental lists + author: John Doe + patat: + incrementalLists: true + ... + + - This list + - is displayed + - item by item + +Setting `incrementalLists` works on _all_ lists in the presentation. To flip +the setting for a specific list, wrap it in a block quote. This will make the +list incremental if `incrementalLists` is not set, and it will display the list +all at once if `incrementalLists` is set to `true`. + +This example contains a sublist which is also displayed incrementally, and then +a sublist which is displayed all at once (by merit of the block quote). + + --- + title: Presentation with incremental lists + author: John Doe + patat: + incrementalLists: true + ... + + - This list + - is displayed + + * item + * by item + + - Or sometimes + + > * all at + > * once + +Another way to break up slides is to use a pagraph only containing three dots +separated by spaces. For example, this slide has two pauses: + + Legen + + . . . + + wait for it + + . . . + + Dary! + +### Theming + +Colors and other properties can also be changed using this configuration. For +example, we can have: + + --- + author: 'Jasper Van der Jeugt' + title: 'This is a test' + patat: + wrap: true + theme: + emph: [vividBlue, onVividBlack, bold] + imageTarget: [onDullWhite, vividRed] + ... + + # This is a presentation + + This is _emph_ text. + + ![Hello](foo.png) + +The properties that can be given a list of styles are: + +`blockQuote`, `borders`, `bulletList`, `codeBlock`, `code`, `definitionList`, +`definitionTerm`, `emph`, `header`, `imageTarget`, `imageText`, `linkTarget`, +`linkText`, `math`, `orderedList`, `quoted`, `strikeout`, `strong`, +`tableHeader`, `tableSeparator` + +The accepted styles are: + +`bold`, `dullBlack`, `dullBlue`, `dullCyan`, `dullGreen`, `dullMagenta`, +`dullRed`, `dullWhite`, `dullYellow`, `onDullBlack`, `onDullBlue`, `onDullCyan`, +`onDullGreen`, `onDullMagenta`, `onDullRed`, `onDullWhite`, `onDullYellow`, +`onVividBlack`, `onVividBlue`, `onVividCyan`, `onVividGreen`, `onVividMagenta`, +`onVividRed`, `onVividWhite`, `onVividYellow`, `underline`, `vividBlack`, +`vividBlue`, `vividCyan`, `vividGreen`, `vividMagenta`, `vividRed`, +`vividWhite`, `vividYellow` + +### Syntax Highlighting + +As part of theming, syntax highlighting is also configurable. This can be +configured like this: + + --- + patat: + theme: + syntaxHighlighting: + decVal: [bold, onDullRed] + ... + + ... + +`decVal` refers to "decimal values". This is known as a "token type". For a +full list of token types, see [this list] -- the names are derived from there in +an obvious way. + +[this list]: https://hackage.haskell.org/package/highlighting-kate-0.6.3/docs/Text-Highlighting-Kate-Types.html#t:TokenType + +Trivia +------ + +_"Patat"_ is the Flemish word for a simple potato. Dutch people also use it to +refer to French Fries but I don't really do that -- in Belgium we just call +fries _"Frieten"_. + +The idea of `patat` is largely based upon [MDP] which is in turn based upon +[VTMC]. I wanted to write a clone using Pandoc because I ran into a markdown +parsing bug in MDP which I could not work around. A second reason to do a +Pandoc-based tool was that I would be able to use [Literate Haskell] as well. +Lastly, I also prefer not to install Node.js on my machine if I can avoid it. + +[MDP]: https://github.com/visit1985/mdp +[VTMC]: https://github.com/jclulow/vtmc +[Literate Haskell]: https://wiki.haskell.org/Literate_programming diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/extra/make-man.hs b/extra/make-man.hs new file mode 100644 index 0000000..78c01f8 --- /dev/null +++ b/extra/make-man.hs @@ -0,0 +1,110 @@ +-- | This script generates a man page for patat. +{-# LANGUAGE CPP #-} +import Control.Applicative ((<$>)) +import Control.Monad (guard) +import Data.Char (isSpace, toLower) +import Data.List (isPrefixOf) +import Data.Maybe (isJust) +import qualified GHC.IO.Encoding as Encoding +import System.Environment (getEnv) +import qualified System.IO as IO +import qualified Text.Pandoc as Pandoc +import qualified Text.Pandoc.Walk as Pandoc +import Prelude + +getVersion :: IO String +getVersion = + dropWhile isSpace . drop 1 . dropWhile (/= ':') . head . + filter (\l -> "version:" `isPrefixOf` map toLower l) . + map (dropWhile isSpace) . lines <$> readFile "patat.cabal" + +removeLinks :: Pandoc.Pandoc -> Pandoc.Pandoc +removeLinks = Pandoc.walk $ \inline -> case inline of + Pandoc.Link _ inlines _ -> Pandoc.Emph inlines + _ -> inline + +type Sections = [(Int, String, [Pandoc.Block])] + +toSections :: Int -> [Pandoc.Block] -> Sections +toSections level = go + where + go [] = [] + go (h : xs) = case toSectionHeader h of + Nothing -> go xs + Just (l, title) -> + let (section, cont) = break (isJust . toSectionHeader) xs in + (l, title, section) : go cont + + toSectionHeader :: Pandoc.Block -> Maybe (Int, String) + toSectionHeader (Pandoc.Header l _ inlines) = do + guard (l <= level) + let doc = Pandoc.Pandoc Pandoc.nullMeta [Pandoc.Plain inlines] + return (l, Pandoc.writeMarkdown Pandoc.def doc) + toSectionHeader _ = Nothing + +fromSections :: Sections -> [Pandoc.Block] +fromSections = concatMap $ \(level, title, blocks) -> + Pandoc.Header level ("", [], []) [Pandoc.Str title] : blocks + +reorganizeSections :: Pandoc.Pandoc -> Pandoc.Pandoc +reorganizeSections (Pandoc.Pandoc meta0 blocks0) = + let sections0 = toSections 2 blocks0 in + Pandoc.Pandoc meta0 $ fromSections $ + [ (1, "NAME", nameSection) + ] ++ + [ (1, "SYNOPSIS", s) + | (_, _, s) <- lookupSection "Running" sections0 + ] ++ + [ (1, "DESCRIPTION", []) + ] ++ + [ (2, n, s) + | (_, n, s) <- lookupSection "Controls" sections0 + ] ++ + [ (2, n, s) + | (_, n, s) <- lookupSection "Input format" sections0 + ] ++ + [ (2, n, s) + | (_, n, s) <- lookupSection "Configuration" sections0 + ] ++ + [ (1, "OPTIONS", s) + | (_, _, s) <- lookupSection "Options" sections0 + ] ++ + [ (1, "SEE ALSO", seeAlsoSection) + ] + where + nameSection = mkPara "patat - Presentations Atop The ANSI Terminal" + seeAlsoSection = mkPara "pandoc(1)" + mkPara str = [Pandoc.Para [Pandoc.Str str]] + + lookupSection name sections = + [section | section@(_, n, _) <- sections, name == n] + +main :: IO () +main = do + Encoding.setLocaleEncoding Encoding.utf8 + Right pandoc0 <- Pandoc.readMarkdown Pandoc.def <$> readFile "README.md" + Right template <- Pandoc.getDefaultTemplate Nothing "man" + + version <- getVersion + date <- getEnv "SOURCE_DATE" + + let writerOptions = Pandoc.def { +#if PANDOC_MINOR_VERSION >= 19 + Pandoc.writerTemplate = Just template +#else + Pandoc.writerStandalone = True + , Pandoc.writerTemplate = template +#endif + , Pandoc.writerVariables = + [ ("author", "Jasper Van der Jeugt") + , ("title", "patat manual") + , ("date", date) + , ("footer", "patat v" ++ version) + , ("section", "1") + ] + } + + let pandoc1 = reorganizeSections $ removeLinks pandoc0 + + putStr $ Pandoc.writeMan writerOptions pandoc1 + IO.hPutStrLn IO.stderr "Wrote man page." diff --git a/extra/screenshot.png b/extra/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..e20d771262ad03e0c19be3ba5cd749955c049e5e GIT binary patch literal 52076 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 +Maintainer: Jasper Van der Jeugt +Homepage: http://github.com/jaspervdj/patat +Copyright: 2016 Jasper Van der Jeugt +Category: Text +Build-type: Simple +Extra-source-files: CHANGELOG.md +Cabal-version: >=1.10 + +Source-repository head + Type: git + Location: git://github.com/jaspervdj/patat.git + +Executable patat + Main-is: Main.hs + Ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N" + Hs-source-dirs: src + Default-language: Haskell2010 + + Build-depends: + aeson >= 0.9 && < 1.2, + ansi-terminal >= 0.6 && < 0.7, + ansi-wl-pprint >= 0.6 && < 0.7, + base >= 4.6 && < 4.10, + bytestring >= 0.10 && < 0.11, + containers >= 0.5 && < 0.6, + directory >= 1.2 && < 1.4, + filepath >= 1.4 && < 1.5, + highlighting-kate >= 0.6 && < 0.7, + mtl >= 2.2 && < 2.3, + optparse-applicative >= 0.12 && < 0.14, + pandoc >= 1.16 && < 1.20, + terminal-size >= 0.3 && < 0.4, + text >= 1.2 && < 1.3, + time >= 1.4 && < 1.8, + unordered-containers >= 0.2 && < 0.3, + yaml >= 0.7 && < 0.9 + + Other-modules: + Data.Aeson.Extended + Data.Aeson.TH.Extended + Data.Data.Extended + Patat.AutoAdvance + Patat.Presentation + Patat.Presentation.Display + Patat.Presentation.Display.CodeBlock + Patat.Presentation.Display.Table + Patat.Presentation.Fragment + Patat.Presentation.Interactive + Patat.Presentation.Internal + Patat.Presentation.Read + Patat.PrettyPrint + Patat.Theme + Text.Pandoc.Extended diff --git a/src/Data/Aeson/Extended.hs b/src/Data/Aeson/Extended.hs new file mode 100644 index 0000000..9b95cec --- /dev/null +++ b/src/Data/Aeson/Extended.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +module Data.Aeson.Extended + ( module Data.Aeson + + , FlexibleNum (..) + ) where + +import Control.Applicative ((<$>)) +import Data.Aeson +import qualified Data.Text as T +import Text.Read (readMaybe) +import Prelude + +-- | This can be parsed from a JSON string in addition to a JSON number. +newtype FlexibleNum a = FlexibleNum {unFlexibleNum :: a} + deriving (Show, ToJSON) + +instance (FromJSON a, Read a) => FromJSON (FlexibleNum a) where + parseJSON (String str) = case readMaybe (T.unpack str) of + Nothing -> fail $ "Could not parse " ++ T.unpack str ++ " as a number" + Just x -> return (FlexibleNum x) + parseJSON val = FlexibleNum <$> parseJSON val diff --git a/src/Data/Aeson/TH/Extended.hs b/src/Data/Aeson/TH/Extended.hs new file mode 100644 index 0000000..0fa5487 --- /dev/null +++ b/src/Data/Aeson/TH/Extended.hs @@ -0,0 +1,21 @@ +-------------------------------------------------------------------------------- +module Data.Aeson.TH.Extended + ( module Data.Aeson.TH + , dropPrefixOptions + ) where + + +-------------------------------------------------------------------------------- +import Data.Aeson.TH +import Data.Char (isUpper, toLower) + + +-------------------------------------------------------------------------------- +dropPrefixOptions :: Options +dropPrefixOptions = defaultOptions + { fieldLabelModifier = dropPrefix + } + where + dropPrefix str = case break isUpper str of + (_, (y : ys)) -> toLower y : ys + _ -> str diff --git a/src/Data/Data/Extended.hs b/src/Data/Data/Extended.hs new file mode 100644 index 0000000..636591e --- /dev/null +++ b/src/Data/Data/Extended.hs @@ -0,0 +1,23 @@ +module Data.Data.Extended + ( module Data.Data + + , grecQ + , grecT + ) where + +import Data.Data + +-- | Recursively find all values of a certain type. +grecQ :: (Data a, Data b) => a -> [b] +grecQ = concat . gmapQ (\x -> maybe id (:) (cast x) $ grecQ x) + +-- | Recursively apply an update to a certain type. +grecT :: (Data a, Data b) => (a -> a) -> b -> b +grecT f x = gmapT (grecT f) (castMap f x) + +castMap :: (Typeable a, Typeable b) => (a -> a) -> b -> b +castMap f x = case cast x of + Nothing -> x + Just y -> case cast (f y) of + Nothing -> x + Just z -> z diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..0fccfde --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,181 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Main where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<$>), (<*>)) +import Control.Concurrent (forkIO, threadDelay) +import qualified Control.Concurrent.Chan as Chan +import Control.Exception (finally) +import Control.Monad (forever, unless, when) +import qualified Data.Aeson.Extended as A +import Data.Monoid (mempty, (<>)) +import Data.Time (UTCTime) +import Data.Version (showVersion) +import qualified Options.Applicative as OA +import Patat.AutoAdvance +import Patat.Presentation +import qualified Paths_patat +import Prelude +import qualified System.Console.ANSI as Ansi +import System.Directory (doesFileExist, + getModificationTime) +import System.Exit (exitFailure, exitSuccess) +import qualified System.IO as IO +import qualified Text.PrettyPrint.ANSI.Leijen as PP + + +-------------------------------------------------------------------------------- +data Options = Options + { oFilePath :: !(Maybe FilePath) + , oForce :: !Bool + , oDump :: !Bool + , oWatch :: !Bool + , oVersion :: !Bool + } deriving (Show) + + +-------------------------------------------------------------------------------- +parseOptions :: OA.Parser Options +parseOptions = Options + <$> (OA.optional $ OA.strArgument $ + OA.metavar "FILENAME" <> + OA.help "Input file") + <*> (OA.switch $ + OA.long "force" <> + OA.short 'f' <> + OA.help "Force ANSI terminal" <> + OA.hidden) + <*> (OA.switch $ + OA.long "dump" <> + OA.short 'd' <> + OA.help "Just dump all slides and exit" <> + OA.hidden) + <*> (OA.switch $ + OA.long "watch" <> + OA.short 'w' <> + OA.help "Watch file for changes") + <*> (OA.switch $ + OA.long "version" <> + OA.help "Display version info and exit" <> + OA.hidden) + + +-------------------------------------------------------------------------------- +parserInfo :: OA.ParserInfo Options +parserInfo = OA.info (OA.helper <*> parseOptions) $ + OA.fullDesc <> + OA.header ("patat v" <> showVersion Paths_patat.version) <> + OA.progDescDoc (Just desc) + where + desc = PP.vcat + [ "Terminal-based presentations using Pandoc" + , "" + , "Controls:" + , "- Next slide: space, enter, l, right" + , "- Previous slide: backspace, h, left" + , "- Go forward 10 slides: j, down" + , "- Go backward 10 slides: k, up" + , "- First slide: 0" + , "- Last slide: G" + , "- Reload file: r" + , "- Quit: q" + ] + + +-------------------------------------------------------------------------------- +parserPrefs :: OA.ParserPrefs +parserPrefs = OA.prefs OA.showHelpOnError + + +-------------------------------------------------------------------------------- +errorAndExit :: [String] -> IO a +errorAndExit msg = do + mapM_ (IO.hPutStrLn IO.stderr) msg + exitFailure + + +-------------------------------------------------------------------------------- +assertAnsiFeatures :: IO () +assertAnsiFeatures = do + supports <- Ansi.hSupportsANSI IO.stdout + unless supports $ errorAndExit + [ "It looks like your terminal does not support ANSI codes." + , "If you still want to run the presentation, use `--force`." + ] + + +-------------------------------------------------------------------------------- +main :: IO () +main = do + options <- OA.customExecParser parserPrefs parserInfo + + when (oVersion options) $ do + putStrLn (showVersion Paths_patat.version) + exitSuccess + + filePath <- case oFilePath options of + Just fp -> return fp + Nothing -> OA.handleParseResult $ OA.Failure $ + OA.parserFailure parserPrefs parserInfo OA.ShowHelpText mempty + + errOrPres <- readPresentation filePath + pres <- either (errorAndExit . return) return errOrPres + + unless (oForce options) assertAnsiFeatures + + if oDump options + then dumpPresentation pres + else interactiveLoop options pres + where + interactiveLoop :: Options -> Presentation -> IO () + interactiveLoop options pres0 = (`finally` Ansi.showCursor) $ do + IO.hSetBuffering IO.stdin IO.NoBuffering + Ansi.hideCursor + + -- Spawn the initial channel that gives us commands based on user input. + commandChan0 <- Chan.newChan + _ <- forkIO $ forever $ + readPresentationCommand >>= Chan.writeChan commandChan0 + + -- If an auto delay is set, use 'autoAdvance' to create a new one. + commandChan <- case psAutoAdvanceDelay (pSettings pres0) of + Nothing -> return commandChan0 + Just (A.FlexibleNum delay) -> autoAdvance delay commandChan0 + + -- Spawn a thread that adds 'Reload' commands based on the file time. + mtime0 <- getModificationTime (pFilePath pres0) + when (oWatch options) $ do + _ <- forkIO $ watcher commandChan (pFilePath pres0) mtime0 + return () + + let loop :: Presentation -> Maybe String -> IO () + loop pres mbError = do + case mbError of + Nothing -> displayPresentation pres + Just err -> displayPresentationError pres err + + c <- Chan.readChan commandChan + update <- updatePresentation c pres + case update of + ExitedPresentation -> return () + UpdatedPresentation pres' -> loop pres' Nothing + ErroredPresentation err -> loop pres (Just err) + + loop pres0 Nothing + + +-------------------------------------------------------------------------------- +watcher :: Chan.Chan PresentationCommand -> FilePath -> UTCTime -> IO a +watcher chan filePath mtime0 = do + -- The extra exists check helps because some editors temporarily make the + -- file dissapear while writing. + exists <- doesFileExist filePath + mtime1 <- if exists then getModificationTime filePath else return mtime0 + + when (mtime1 > mtime0) $ Chan.writeChan chan Reload + threadDelay (200 * 1000) + watcher chan filePath mtime1 diff --git a/src/Patat/AutoAdvance.hs b/src/Patat/AutoAdvance.hs new file mode 100644 index 0000000..236e0cb --- /dev/null +++ b/src/Patat/AutoAdvance.hs @@ -0,0 +1,52 @@ +-------------------------------------------------------------------------------- +module Patat.AutoAdvance + ( autoAdvance + ) where + + +-------------------------------------------------------------------------------- +import Control.Concurrent (forkIO, threadDelay) +import qualified Control.Concurrent.Chan as Chan +import Control.Monad (forever) +import qualified Data.IORef as IORef +import Data.Time (diffUTCTime, getCurrentTime) +import Patat.Presentation (PresentationCommand (..)) + + +-------------------------------------------------------------------------------- +-- | This function takes an existing channel for presentation commands +-- (presumably coming from human input) and creates a new one that /also/ sends +-- a 'Forward' command if nothing happens for N seconds. +autoAdvance + :: Int + -> Chan.Chan PresentationCommand + -> IO (Chan.Chan PresentationCommand) +autoAdvance delaySeconds existingChan = do + let delay = delaySeconds * 1000 -- We are working with ms in this function + + newChan <- Chan.newChan + latestCommandAt <- IORef.newIORef =<< getCurrentTime + + -- This is a thread that copies 'existingChan' to 'newChan', and writes + -- whenever the latest command was to 'latestCommandAt'. + _ <- forkIO $ forever $ do + cmd <- Chan.readChan existingChan + getCurrentTime >>= IORef.writeIORef latestCommandAt + Chan.writeChan newChan cmd + + -- This is a thread that waits around 'delay' seconds and then checks if + -- there's been a more recent command. If not, we write a 'Forward'. + _ <- forkIO $ forever $ do + current <- getCurrentTime + latest <- IORef.readIORef latestCommandAt + let elapsed = floor $ 1000 * (current `diffUTCTime` latest) :: Int + if elapsed >= delay + then do + Chan.writeChan newChan Forward + IORef.writeIORef latestCommandAt current + threadDelay (delay * 1000) + else do + let wait = delay - elapsed + threadDelay (wait * 1000) + + return newChan diff --git a/src/Patat/Presentation.hs b/src/Patat/Presentation.hs new file mode 100644 index 0000000..8da5a30 --- /dev/null +++ b/src/Patat/Presentation.hs @@ -0,0 +1,20 @@ +module Patat.Presentation + ( PresentationSettings (..) + , defaultPresentationSettings + + , Presentation (..) + , readPresentation + , displayPresentation + , displayPresentationError + , dumpPresentation + + , PresentationCommand (..) + , readPresentationCommand + , UpdatedPresentation (..) + , updatePresentation + ) where + +import Patat.Presentation.Display +import Patat.Presentation.Interactive +import Patat.Presentation.Internal +import Patat.Presentation.Read diff --git a/src/Patat/Presentation/Display.hs b/src/Patat/Presentation/Display.hs new file mode 100644 index 0000000..cb562d7 --- /dev/null +++ b/src/Patat/Presentation/Display.hs @@ -0,0 +1,313 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE CPP #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display + ( displayPresentation + , displayPresentationError + , dumpPresentation + ) where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<$>)) +import Control.Monad (mplus, unless) +import qualified Data.Aeson.Extended as A +import Data.Data.Extended (grecQ) +import Data.List (intersperse) +import Data.Maybe (fromMaybe) +import Data.Monoid (mconcat, mempty, (<>)) +import qualified Data.Text as T +import Patat.Presentation.Display.CodeBlock +import Patat.Presentation.Display.Table +import Patat.Presentation.Internal +import Patat.PrettyPrint ((<$$>), (<+>)) +import qualified Patat.PrettyPrint as PP +import Patat.Theme (Theme (..)) +import qualified Patat.Theme as Theme +import Prelude +import qualified System.Console.ANSI as Ansi +import qualified System.Console.Terminal.Size as Terminal +import qualified System.IO as IO +import qualified Text.Pandoc.Extended as Pandoc + + +-------------------------------------------------------------------------------- +-- | Display something within the presentation borders that draw the title and +-- the active slide number and so on. +displayWithBorders :: Presentation -> (Theme -> PP.Doc) -> IO () +displayWithBorders Presentation {..} f = do + Ansi.clearScreen + Ansi.setCursorPosition 0 0 + + -- Get terminal width/title + mbWindow <- Terminal.size + let columns = fromMaybe 72 $ + (A.unFlexibleNum <$> psColumns pSettings) `mplus` + (Terminal.width <$> mbWindow) + rows = fromMaybe 24 $ + (A.unFlexibleNum <$> psRows pSettings) `mplus` + (Terminal.height <$> mbWindow) + + let settings = pSettings {psColumns = Just $ A.FlexibleNum columns} + theme = fromMaybe Theme.defaultTheme (psTheme settings) + title = PP.toString (prettyInlines theme pTitle) + titleWidth = length title + titleOffset = (columns - titleWidth) `div` 2 + borders = themed (themeBorders theme) + + unless (null title) $ do + Ansi.setCursorColumn titleOffset + PP.putDoc $ borders $ PP.string title + putStrLn "" + putStrLn "" + + PP.putDoc $ withWrapSettings settings $ f theme + putStrLn "" + + let (sidx, _) = pActiveFragment + active = show (sidx + 1) ++ " / " ++ show (length pSlides) + activeWidth = length active + + Ansi.setCursorPosition (rows - 1) 0 + PP.putDoc $ " " <> borders (prettyInlines theme pAuthor) + Ansi.setCursorColumn (columns - activeWidth - 1) + PP.putDoc $ borders $ PP.string active + IO.hFlush IO.stdout + + +-------------------------------------------------------------------------------- +displayPresentation :: Presentation -> IO () +displayPresentation pres@Presentation {..} = displayWithBorders pres $ \theme -> + let fragment = fromMaybe mempty (getActiveFragment pres) in + prettyFragment theme fragment + + +-------------------------------------------------------------------------------- +-- | Displays an error in the place of the presentation. This is useful if we +-- want to display an error but keep the presentation running. +displayPresentationError :: Presentation -> String -> IO () +displayPresentationError pres err = displayWithBorders pres $ \Theme {..} -> + themed themeStrong "Error occurred in the presentation:" <$$> + "" <$$> + (PP.string err) + + +-------------------------------------------------------------------------------- +dumpPresentation :: Presentation -> IO () +dumpPresentation pres = + let theme = fromMaybe Theme.defaultTheme (psTheme $ pSettings pres) in + PP.putDoc $ withWrapSettings (pSettings pres) $ + PP.vcat $ intersperse "----------" $ do + Slide fragments <- pSlides pres + return $ PP.vcat $ intersperse "~~~~~~~~~~" $ do + fragment <- fragments + return $ prettyFragment theme fragment + + +-------------------------------------------------------------------------------- +withWrapSettings :: PresentationSettings -> PP.Doc -> PP.Doc +withWrapSettings ps = case (psWrap ps, psColumns ps) of + (Just True, Just (A.FlexibleNum col)) -> PP.wrapAt (Just col) + _ -> id + + +-------------------------------------------------------------------------------- +prettyFragment :: Theme -> Fragment -> PP.Doc +prettyFragment theme fragment@(Fragment blocks) = + prettyBlocks theme blocks <> + case prettyReferences theme fragment of + [] -> mempty + refs -> PP.hardline <> PP.vcat refs + + +-------------------------------------------------------------------------------- +prettyBlock :: Theme -> Pandoc.Block -> PP.Doc + +prettyBlock theme (Pandoc.Plain inlines) = prettyInlines theme inlines + +prettyBlock theme (Pandoc.Para inlines) = + prettyInlines theme inlines <> PP.hardline + +prettyBlock theme@Theme {..} (Pandoc.Header i _ inlines) = + themed themeHeader (PP.string (replicate i '#') <+> prettyInlines theme inlines) <> + PP.hardline + +prettyBlock theme (Pandoc.CodeBlock (_, classes, _) txt) = + prettyCodeBlock theme classes txt + +prettyBlock theme (Pandoc.BulletList bss) = PP.vcat + [ PP.indent + (PP.NotTrimmable $ themed (themeBulletList theme) prefix) + (PP.Trimmable " ") + (prettyBlocks theme' bs) + | bs <- bss + ] <> PP.hardline + where + prefix = " " <> PP.string [marker] <> " " + marker = case T.unpack <$> themeBulletListMarkers theme of + Just (x : _) -> x + _ -> '-' + + -- Cycle the markers. + theme' = theme + { themeBulletListMarkers = + (\ls -> T.drop 1 ls <> T.take 1 ls) <$> themeBulletListMarkers theme + } + +prettyBlock theme@Theme {..} (Pandoc.OrderedList _ bss) = PP.vcat + [ PP.indent + (PP.NotTrimmable $ themed themeOrderedList $ PP.string prefix) + (PP.Trimmable " ") + (prettyBlocks theme bs) + | (prefix, bs) <- zip padded bss + ] <> PP.hardline + where + padded = [n ++ replicate (4 - length n) ' ' | n <- numbers] + numbers = + [ show i ++ "." + | i <- [1 .. length bss] + ] + +prettyBlock _theme (Pandoc.RawBlock _ t) = PP.string t <> PP.hardline + +prettyBlock _theme Pandoc.HorizontalRule = "---" + +prettyBlock theme@Theme {..} (Pandoc.BlockQuote bs) = + let quote = PP.NotTrimmable (themed themeBlockQuote "> ") in + PP.indent quote quote (prettyBlocks theme bs) + +prettyBlock theme@Theme {..} (Pandoc.DefinitionList terms) = + PP.vcat $ map prettyDefinition terms + where + prettyDefinition (term, definitions) = + themed themeDefinitionTerm (prettyInlines theme term) <$$> + PP.hardline <> PP.vcat + [ PP.indent + (PP.NotTrimmable (themed themeDefinitionList ": ")) + (PP.Trimmable " ") $ + prettyBlocks theme (Pandoc.plainToPara definition) + | definition <- definitions + ] + +prettyBlock theme (Pandoc.Table caption aligns _ headers rows) = + PP.wrapAt Nothing $ + prettyTable theme Table + { tCaption = prettyInlines theme caption + , tAligns = map align aligns + , tHeaders = map (prettyBlocks theme) headers + , tRows = map (map (prettyBlocks theme)) rows + } + where + align Pandoc.AlignLeft = PP.AlignLeft + align Pandoc.AlignCenter = PP.AlignCenter + align Pandoc.AlignDefault = PP.AlignLeft + align Pandoc.AlignRight = PP.AlignRight + +prettyBlock theme (Pandoc.Div _attrs blocks) = prettyBlocks theme blocks + +prettyBlock _theme Pandoc.Null = mempty + +#if MIN_VERSION_pandoc(1,18,0) +-- 'LineBlock' elements are new in pandoc-1.18 +prettyBlock theme@Theme {..} (Pandoc.LineBlock inliness) = + let ind = PP.NotTrimmable (themed themeLineBlock "| ") in + PP.wrapAt Nothing $ + PP.indent ind ind $ + PP.vcat $ + map (prettyInlines theme) inliness +#endif + + +-------------------------------------------------------------------------------- +prettyBlocks :: Theme -> [Pandoc.Block] -> PP.Doc +prettyBlocks theme = PP.vcat . map (prettyBlock theme) + + +-------------------------------------------------------------------------------- +prettyInline :: Theme -> Pandoc.Inline -> PP.Doc + +prettyInline _theme Pandoc.Space = PP.space + +prettyInline _theme (Pandoc.Str str) = PP.string str + +prettyInline theme@Theme {..} (Pandoc.Emph inlines) = + themed themeEmph $ + prettyInlines theme inlines + +prettyInline theme@Theme {..} (Pandoc.Strong inlines) = + themed themeStrong $ + prettyInlines theme inlines + +prettyInline Theme {..} (Pandoc.Code _ txt) = + themed themeCode $ + " " <> PP.string txt <> " " + +prettyInline theme@Theme {..} link@(Pandoc.Link _attrs text (target, _title)) + | isReferenceLink link = + "[" <> themed themeLinkText (prettyInlines theme text) <> "]" + | otherwise = + "<" <> themed themeLinkTarget (PP.string target) <> ">" + +prettyInline _theme Pandoc.SoftBreak = PP.softline + +prettyInline _theme Pandoc.LineBreak = PP.hardline + +prettyInline theme@Theme {..} (Pandoc.Strikeout t) = + "~~" <> themed themeStrikeout (prettyInlines theme t) <> "~~" + +prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.SingleQuote t) = + "'" <> themed themeQuoted (prettyInlines theme t) <> "'" +prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.DoubleQuote t) = + "'" <> themed themeQuoted (prettyInlines theme t) <> "'" + +prettyInline Theme {..} (Pandoc.Math _ t) = + themed themeMath (PP.string t) + +prettyInline theme@Theme {..} (Pandoc.Image _attrs text (target, _title)) = + "![" <> themed themeImageText (prettyInlines theme text) <> "](" <> + themed themeImageTarget (PP.string target) <> ")" + +-- These elements aren't really supported. +prettyInline theme (Pandoc.Cite _ t) = prettyInlines theme t +prettyInline theme (Pandoc.Span _ t) = prettyInlines theme t +prettyInline _theme (Pandoc.RawInline _ t) = PP.string t +prettyInline theme (Pandoc.Note t) = prettyBlocks theme t +prettyInline theme (Pandoc.Superscript t) = prettyInlines theme t +prettyInline theme (Pandoc.Subscript t) = prettyInlines theme t +prettyInline theme (Pandoc.SmallCaps t) = prettyInlines theme t +-- prettyInline unsupported = PP.ondullred $ PP.string $ show unsupported + + +-------------------------------------------------------------------------------- +prettyInlines :: Theme -> [Pandoc.Inline] -> PP.Doc +prettyInlines theme = mconcat . map (prettyInline theme) + + +-------------------------------------------------------------------------------- +prettyReferences :: Theme -> Fragment -> [PP.Doc] +prettyReferences theme@Theme {..} = + map prettyReference . getReferences . unFragment + where + getReferences :: [Pandoc.Block] -> [Pandoc.Inline] + getReferences = filter isReferenceLink . grecQ + + prettyReference :: Pandoc.Inline -> PP.Doc + prettyReference (Pandoc.Link _attrs text (target, title)) = + "[" <> + themed themeLinkText (prettyInlines theme $ Pandoc.newlineToSpace text) <> + "](" <> + themed themeLinkTarget (PP.string target) <> + (if null title + then mempty + else PP.space <> "\"" <> PP.string title <> "\"") + <> ")" + prettyReference _ = mempty + + +-------------------------------------------------------------------------------- +isReferenceLink :: Pandoc.Inline -> Bool +isReferenceLink (Pandoc.Link _attrs text (target, _)) = + [Pandoc.Str target] /= text +isReferenceLink _ = False diff --git a/src/Patat/Presentation/Display/CodeBlock.hs b/src/Patat/Presentation/Display/CodeBlock.hs new file mode 100644 index 0000000..4888166 --- /dev/null +++ b/src/Patat/Presentation/Display/CodeBlock.hs @@ -0,0 +1,79 @@ +-------------------------------------------------------------------------------- +-- | Displaying code blocks, optionally with syntax highlighting. +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display.CodeBlock + ( prettyCodeBlock + ) where + + +-------------------------------------------------------------------------------- +import Data.Char (toLower) +import Data.List (find) +import Data.Monoid (mconcat, (<>)) +import qualified Data.Set as S +import Patat.Presentation.Display.Table (themed) +import qualified Patat.PrettyPrint as PP +import Patat.Theme +import qualified Text.Highlighting.Kate as Kate +import Prelude + + +-------------------------------------------------------------------------------- +lower :: String -> String +lower = map toLower + + +-------------------------------------------------------------------------------- +supportedLanguages :: S.Set String +supportedLanguages = S.fromList (map lower Kate.languages) + + +-------------------------------------------------------------------------------- +highlight :: [String] -> String -> [Kate.SourceLine] +highlight classes rawCodeBlock = + case find (\c -> lower c `S.member` supportedLanguages) classes of + Nothing -> zeroHighlight rawCodeBlock + Just lang -> Kate.highlightAs lang rawCodeBlock + + +-------------------------------------------------------------------------------- +-- | This does fake highlighting, everything becomes a normal token. That makes +-- things a bit easier, since we only need to deal with one cases in the +-- renderer. +zeroHighlight :: String -> [Kate.SourceLine] +zeroHighlight str = [[(Kate.NormalTok, line)] | line <- lines str] + + +-------------------------------------------------------------------------------- +prettyCodeBlock :: Theme -> [String] -> String -> PP.Doc +prettyCodeBlock theme@Theme {..} classes rawCodeBlock = + PP.vcat (map blockified sourceLines) <> + PP.hardline + where + sourceLines :: [Kate.SourceLine] + sourceLines = + [[]] ++ highlight classes rawCodeBlock ++ [[]] + + prettySourceLine :: Kate.SourceLine -> PP.Doc + prettySourceLine = mconcat . map prettyToken + + prettyToken :: Kate.Token -> PP.Doc + prettyToken (tokenType, str) = + themed (syntaxHighlight theme tokenType) (PP.string str) + + sourceLineLength :: Kate.SourceLine -> Int + sourceLineLength line = sum [length str | (_, str) <- line] + + blockWidth :: Int + blockWidth = foldr max 0 (map sourceLineLength sourceLines) + + blockified :: Kate.SourceLine -> PP.Doc + blockified line = + let len = sourceLineLength line + indent = PP.NotTrimmable " " in + PP.indent indent indent $ + themed themeCodeBlock $ + " " <> + prettySourceLine line <> + PP.string (replicate (blockWidth - len) ' ') <> " " diff --git a/src/Patat/Presentation/Display/Table.hs b/src/Patat/Presentation/Display/Table.hs new file mode 100644 index 0000000..fee68c9 --- /dev/null +++ b/src/Patat/Presentation/Display/Table.hs @@ -0,0 +1,107 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display.Table + ( Table (..) + , prettyTable + + , themed + ) where + + +-------------------------------------------------------------------------------- +import Data.List (intersperse, transpose) +import Data.Monoid (mconcat, mempty, (<>)) +import Patat.PrettyPrint ((<$$>)) +import qualified Patat.PrettyPrint as PP +import Patat.Theme (Theme (..)) +import qualified Patat.Theme as Theme +import Prelude + + +-------------------------------------------------------------------------------- +data Table = Table + { tCaption :: PP.Doc + , tAligns :: [PP.Alignment] + , tHeaders :: [PP.Doc] + , tRows :: [[PP.Doc]] + } + + +-------------------------------------------------------------------------------- +prettyTable + :: Theme -> Table -> PP.Doc +prettyTable theme@Theme {..} Table {..} = + PP.indent (PP.Trimmable " ") (PP.Trimmable " ") $ + lineIf (not isHeaderLess) (hcat2 headerHeight + [ themed themeTableHeader (PP.align w a (vpad headerHeight header)) + | (w, a, header) <- zip3 columnWidths tAligns tHeaders + ]) <> + dashedHeaderSeparator theme columnWidths <$$> + joinRows + [ hcat2 rowHeight + [ PP.align w a (vpad rowHeight cell) + | (w, a, cell) <- zip3 columnWidths tAligns row + ] + | (rowHeight, row) <- zip rowHeights tRows + ] <$$> + lineIf isHeaderLess (dashedHeaderSeparator theme columnWidths) <> + lineIf + (not $ PP.null tCaption) (PP.hardline <> "Table: " <> tCaption) + where + lineIf cond line = if cond then line <> PP.hardline else mempty + + joinRows + | all (all isSimpleCell) tRows = PP.vcat + | otherwise = PP.vcat . intersperse "" + + isHeaderLess = all PP.null tHeaders + + headerDimensions = map PP.dimensions tHeaders :: [(Int, Int)] + rowDimensions = map (map PP.dimensions) tRows :: [[(Int, Int)]] + + columnWidths :: [Int] + columnWidths = + [ safeMax (map snd col) + | col <- transpose (headerDimensions : rowDimensions) + ] + + rowHeights = map (safeMax . map fst) rowDimensions :: [Int] + headerHeight = safeMax (map fst headerDimensions) :: Int + + vpad :: Int -> PP.Doc -> PP.Doc + vpad height doc = + let (actual, _) = PP.dimensions doc in + doc <> mconcat (replicate (height - actual) PP.hardline) + + safeMax = foldr max 0 + + hcat2 :: Int -> [PP.Doc] -> PP.Doc + hcat2 rowHeight = PP.paste . intersperse (spaces2 rowHeight) + + spaces2 :: Int -> PP.Doc + spaces2 rowHeight = + mconcat $ intersperse PP.hardline $ + replicate rowHeight (PP.string " ") + + +-------------------------------------------------------------------------------- +isSimpleCell :: PP.Doc -> Bool +isSimpleCell = (<= 1) . fst . PP.dimensions + + +-------------------------------------------------------------------------------- +dashedHeaderSeparator :: Theme -> [Int] -> PP.Doc +dashedHeaderSeparator Theme {..} columnWidths = + mconcat $ intersperse (PP.string " ") + [ themed themeTableSeparator (PP.string (replicate w '-')) + | w <- columnWidths + ] + + +-------------------------------------------------------------------------------- +-- | This does not really belong in the module. +themed :: Maybe Theme.Style -> PP.Doc -> PP.Doc +themed Nothing = id +themed (Just (Theme.Style [])) = id +themed (Just (Theme.Style codes)) = PP.ansi codes diff --git a/src/Patat/Presentation/Fragment.hs b/src/Patat/Presentation/Fragment.hs new file mode 100644 index 0000000..0908381 --- /dev/null +++ b/src/Patat/Presentation/Fragment.hs @@ -0,0 +1,134 @@ +-- | For background info on the spec, see the "Incremental lists" section of the +-- the pandoc manual. +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} +module Patat.Presentation.Fragment + ( FragmentSettings (..) + , fragmentBlocks + , fragmentBlock + ) where + +import Data.Foldable (Foldable) +import Data.List (foldl', intersperse) +import Data.Maybe (fromMaybe) +import Data.Traversable (Traversable) +import Prelude +import qualified Text.Pandoc as Pandoc + +data FragmentSettings = FragmentSettings + { fsIncrementalLists :: !Bool + } deriving (Show) + +-- fragmentBlocks :: [Pandoc.Block] -> [[Pandoc.Block]] +-- fragmentBlocks = NonEmpty.toList . joinFragmentedBlocks . map fragmentBlock +fragmentBlocks :: FragmentSettings -> [Pandoc.Block] -> [[Pandoc.Block]] +fragmentBlocks fs blocks0 = + case joinFragmentedBlocks (map (fragmentBlock fs) blocks0) of + Unfragmented bs -> [bs] + Fragmented xs bs -> map (fromMaybe []) xs ++ [fromMaybe [] bs] + +-- | This is all the ways we can "present" a block, after splitting in +-- fragments. +-- +-- In the simplest (and most common case) a block can only be presented in a +-- single way ('Unfragmented'). +-- +-- Alternatively, we might want to show different (partial) versions of the +-- block first before showing the final complete one. These partial or complete +-- versions can be empty, hence the 'Maybe'. +-- +-- For example, imagine that we display the following bullet list incrementally: +-- +-- > [1, 2, 3] +-- +-- Then we would get something like: +-- +-- > Fragmented [Nothing, Just [1], Just [1, 2]] (Just [1, 2, 3]) +data Fragmented a + = Unfragmented a + | Fragmented [Maybe a] (Maybe a) + deriving (Functor, Foldable, Show, Traversable) + +fragmentBlock :: FragmentSettings -> Pandoc.Block -> Fragmented Pandoc.Block +fragmentBlock _fs block@(Pandoc.Para inlines) + | inlines == threeDots = Fragmented [Nothing] Nothing + | otherwise = Unfragmented block + where + threeDots = intersperse Pandoc.Space $ replicate 3 (Pandoc.Str ".") + +fragmentBlock fs (Pandoc.BulletList bs0) = + fragmentList fs (fsIncrementalLists fs) Pandoc.BulletList bs0 + +fragmentBlock fs (Pandoc.OrderedList attr bs0) = + fragmentList fs (fsIncrementalLists fs) (Pandoc.OrderedList attr) bs0 + +fragmentBlock fs (Pandoc.BlockQuote [Pandoc.BulletList bs0]) = + fragmentList fs (not $ fsIncrementalLists fs) Pandoc.BulletList bs0 + +fragmentBlock fs (Pandoc.BlockQuote [Pandoc.OrderedList attr bs0]) = + fragmentList fs (not $ fsIncrementalLists fs) (Pandoc.OrderedList attr) bs0 + +fragmentBlock _ block@(Pandoc.BlockQuote _) = Unfragmented block + +fragmentBlock _ block@(Pandoc.Header _ _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.Plain _) = Unfragmented block +fragmentBlock _ block@(Pandoc.CodeBlock _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.RawBlock _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.DefinitionList _) = Unfragmented block +fragmentBlock _ block@(Pandoc.Table _ _ _ _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.Div _ _) = Unfragmented block +fragmentBlock _ block@Pandoc.HorizontalRule = Unfragmented block +fragmentBlock _ block@Pandoc.Null = Unfragmented block + +#if MIN_VERSION_pandoc(1,18,0) +fragmentBlock _ block@(Pandoc.LineBlock _) = Unfragmented block +#endif + +joinFragmentedBlocks :: [Fragmented block] -> Fragmented [block] +joinFragmentedBlocks = + foldl' append (Unfragmented []) + where + append (Unfragmented xs) (Unfragmented y) = + Unfragmented (xs ++ [y]) + + append (Fragmented xs x) (Unfragmented y) = + Fragmented xs (appendMaybe x (Just y)) + + append (Unfragmented x) (Fragmented ys y) = + Fragmented + [appendMaybe (Just x) y' | y' <- ys] + (appendMaybe (Just x) y) + + append (Fragmented xs x) (Fragmented ys y) = + Fragmented + (xs ++ [appendMaybe x y' | y' <- ys]) + (appendMaybe x y) + + appendMaybe :: Maybe [a] -> Maybe a -> Maybe [a] + appendMaybe Nothing Nothing = Nothing + appendMaybe Nothing (Just x) = Just [x] + appendMaybe (Just xs) Nothing = Just xs + appendMaybe (Just xs) (Just x) = Just (xs ++ [x]) + +fragmentList + :: FragmentSettings -- ^ Global settings + -> Bool -- ^ Fragment THIS list? + -> ([[Pandoc.Block]] -> Pandoc.Block) -- ^ List constructor + -> [[Pandoc.Block]] -- ^ List items + -> Fragmented Pandoc.Block -- ^ Resulting list +fragmentList fs fragmentThisList constructor blocks0 = + fmap constructor fragmented + where + -- The fragmented list per list item. + items :: [Fragmented [Pandoc.Block]] + items = map (joinFragmentedBlocks . map (fragmentBlock fs)) blocks0 + + fragmented :: Fragmented [[Pandoc.Block]] + fragmented = joinFragmentedBlocks $ + map (if fragmentThisList then insertPause else id) items + + insertPause :: Fragmented a -> Fragmented a + insertPause (Unfragmented x) = Fragmented [Nothing] (Just x) + insertPause (Fragmented xs x) = Fragmented (Nothing : xs) x diff --git a/src/Patat/Presentation/Interactive.hs b/src/Patat/Presentation/Interactive.hs new file mode 100644 index 0000000..830f0ff --- /dev/null +++ b/src/Patat/Presentation/Interactive.hs @@ -0,0 +1,122 @@ +-------------------------------------------------------------------------------- +-- | Module that allows the user to interact with the presentation +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Interactive + ( PresentationCommand (..) + , readPresentationCommand + + , UpdatedPresentation (..) + , updatePresentation + ) where + + +-------------------------------------------------------------------------------- +import Patat.Presentation.Internal +import Patat.Presentation.Read + + +-------------------------------------------------------------------------------- +data PresentationCommand + = Exit + | Forward + | Backward + | SkipForward + | SkipBackward + | First + | Last + | Reload + | UnknownCommand String + + +-------------------------------------------------------------------------------- +readPresentationCommand :: IO PresentationCommand +readPresentationCommand = do + k <- readKey + case k of + "q" -> return Exit + "\n" -> return Forward + "\DEL" -> return Backward + "h" -> return Backward + "j" -> return SkipForward + "k" -> return SkipBackward + "l" -> return Forward + "\ESC[C" -> return Forward + "\ESC[D" -> return Backward + "\ESC[B" -> return SkipForward + "\ESC[A" -> return SkipBackward + "0" -> return First + "G" -> return Last + "r" -> return Reload + _ -> return (UnknownCommand k) + where + readKey :: IO String + readKey = do + c0 <- getChar + case c0 of + '\ESC' -> do + c1 <- getChar + case c1 of + '[' -> do + c2 <- getChar + return [c0, c1, c2] + _ -> return [c0, c1] + _ -> return [c0] + + +-------------------------------------------------------------------------------- +data UpdatedPresentation + = UpdatedPresentation !Presentation + | ExitedPresentation + | ErroredPresentation String + deriving (Show) + + +-------------------------------------------------------------------------------- +updatePresentation + :: PresentationCommand -> Presentation -> IO UpdatedPresentation + +updatePresentation cmd presentation = case cmd of + Exit -> return ExitedPresentation + Forward -> return $ goToSlide $ \(s, f) -> (s, f + 1) + Backward -> return $ goToSlide $ \(s, f) -> (s, f - 1) + SkipForward -> return $ goToSlide $ \(s, _) -> (s + 10, 0) + SkipBackward -> return $ goToSlide $ \(s, _) -> (s - 10, 0) + First -> return $ goToSlide $ \_ -> (0, 0) + Last -> return $ goToSlide $ \_ -> (numSlides presentation, 0) + Reload -> reloadPresentation + UnknownCommand _ -> return (UpdatedPresentation presentation) + where + numSlides :: Presentation -> Int + numSlides pres = length (pSlides pres) + + clip :: Index -> Presentation -> Index + clip (slide, fragment) pres + | slide >= numSlides pres = (numSlides pres - 1, lastFragments - 1) + | slide < 0 = (0, 0) + | fragment >= numFragments slide = + if slide + 1 >= numSlides pres + then (slide, lastFragments - 1) + else (slide + 1, 0) + | fragment < 0 = + if slide - 1 >= 0 + then (slide - 1, numFragments (slide - 1) - 1) + else (slide, 0) + | otherwise = (slide, fragment) + where + numFragments s = maybe 1 (length . unSlide) (getSlide s pres) + lastFragments = numFragments (numSlides pres - 1) + + goToSlide :: (Index -> Index) -> UpdatedPresentation + goToSlide f = UpdatedPresentation $ presentation + { pActiveFragment = clip (f $ pActiveFragment presentation) presentation + } + + reloadPresentation = do + errOrPres <- readPresentation (pFilePath presentation) + return $ case errOrPres of + Left err -> ErroredPresentation err + Right pres -> UpdatedPresentation $ pres + { pActiveFragment = clip (pActiveFragment presentation) pres + } diff --git a/src/Patat/Presentation/Internal.hs b/src/Patat/Presentation/Internal.hs new file mode 100644 index 0000000..3554923 --- /dev/null +++ b/src/Patat/Presentation/Internal.hs @@ -0,0 +1,107 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +module Patat.Presentation.Internal + ( Presentation (..) + , PresentationSettings (..) + , defaultPresentationSettings + , Slide (..) + , Fragment (..) + , Index + + , getSlide + , getActiveFragment + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (mplus) +import qualified Data.Aeson.Extended as A +import qualified Data.Aeson.TH.Extended as A +import Data.Maybe (listToMaybe) +import Data.Monoid (Monoid (..), (<>)) +import qualified Patat.Theme as Theme +import qualified Text.Pandoc as Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +data Presentation = Presentation + { pFilePath :: !FilePath + , pTitle :: ![Pandoc.Inline] + , pAuthor :: ![Pandoc.Inline] + , pSettings :: !PresentationSettings + , pSlides :: [Slide] + , pActiveFragment :: !Index + } deriving (Show) + + +-------------------------------------------------------------------------------- +-- | These are patat-specific settings. That is where they differ from more +-- general metadata (author, title...) +data PresentationSettings = PresentationSettings + { psRows :: !(Maybe (A.FlexibleNum Int)) + , psColumns :: !(Maybe (A.FlexibleNum Int)) + , psWrap :: !(Maybe Bool) + , psTheme :: !(Maybe Theme.Theme) + , psIncrementalLists :: !(Maybe Bool) + , psAutoAdvanceDelay :: !(Maybe (A.FlexibleNum Int)) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Monoid PresentationSettings where + mempty = PresentationSettings + Nothing Nothing Nothing Nothing Nothing Nothing + mappend l r = PresentationSettings + { psRows = psRows l `mplus` psRows r + , psColumns = psColumns l `mplus` psColumns r + , psWrap = psWrap l `mplus` psWrap r + , psTheme = psTheme l <> psTheme r + , psIncrementalLists = psIncrementalLists l `mplus` psIncrementalLists r + , psAutoAdvanceDelay = psAutoAdvanceDelay l `mplus` psAutoAdvanceDelay r + } + + +-------------------------------------------------------------------------------- +defaultPresentationSettings :: PresentationSettings +defaultPresentationSettings = PresentationSettings + { psRows = Nothing + , psColumns = Nothing + , psWrap = Nothing + , psTheme = Just Theme.defaultTheme + , psIncrementalLists = Nothing + , psAutoAdvanceDelay = Nothing + } + + +-------------------------------------------------------------------------------- +newtype Slide = Slide {unSlide :: [Fragment]} + deriving (Monoid, Show) + + +-------------------------------------------------------------------------------- +newtype Fragment = Fragment {unFragment :: [Pandoc.Block]} + deriving (Monoid, Show) + + +-------------------------------------------------------------------------------- +-- | Active slide, active fragment. +type Index = (Int, Int) + + +-------------------------------------------------------------------------------- +getSlide :: Int -> Presentation -> Maybe Slide +getSlide sidx = listToMaybe . drop sidx . pSlides + + +-------------------------------------------------------------------------------- +getActiveFragment :: Presentation -> Maybe Fragment +getActiveFragment presentation = do + let (sidx, fidx) = pActiveFragment presentation + Slide fragments <- getSlide sidx presentation + listToMaybe $ drop fidx fragments + + +-------------------------------------------------------------------------------- +$(A.deriveJSON A.dropPrefixOptions ''PresentationSettings) diff --git a/src/Patat/Presentation/Read.hs b/src/Patat/Presentation/Read.hs new file mode 100644 index 0000000..19d357d --- /dev/null +++ b/src/Patat/Presentation/Read.hs @@ -0,0 +1,156 @@ +-- | Read a presentation from disk. +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Read + ( readPresentation + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Except (ExceptT (..), runExceptT, + throwError) +import Control.Monad.Trans (liftIO) +import qualified Data.Aeson as A +import qualified Data.ByteString as B +import qualified Data.HashMap.Strict as HMS +import Data.Maybe (fromMaybe) +import Data.Monoid (mempty, (<>)) +import qualified Data.Set as Set +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Yaml as Yaml +import Patat.Presentation.Fragment +import Patat.Presentation.Internal +import Prelude +import System.Directory (doesFileExist, getHomeDirectory) +import System.FilePath (takeExtension, ()) +import qualified Text.Pandoc.Error as Pandoc +import qualified Text.Pandoc.Extended as Pandoc + + +-------------------------------------------------------------------------------- +readPresentation :: FilePath -> IO (Either String Presentation) +readPresentation filePath = runExceptT $ do + src <- liftIO $ readFile filePath + reader <- case readExtension ext of + Nothing -> throwError $ "Unknown file extension: " ++ show ext + Just x -> return x + doc <- case reader src of + Left e -> throwError $ "Could not parse document: " ++ show e + Right x -> return x + + homeSettings <- ExceptT readHomeSettings + metaSettings <- ExceptT $ return $ readMetaSettings src + let settings = metaSettings <> homeSettings <> defaultPresentationSettings + + ExceptT $ return $ pandocToPresentation filePath settings doc + where + ext = takeExtension filePath + + +-------------------------------------------------------------------------------- +readExtension + :: String -> Maybe (String -> Either Pandoc.PandocError Pandoc.Pandoc) +readExtension fileExt = case fileExt of + ".md" -> Just $ Pandoc.readMarkdown Pandoc.def + ".lhs" -> Just $ Pandoc.readMarkdown lhsOpts + "" -> Just $ Pandoc.readMarkdown Pandoc.def + ".org" -> Just $ Pandoc.readOrg Pandoc.def + _ -> Nothing + + where + lhsOpts = Pandoc.def + { Pandoc.readerExtensions = Set.insert Pandoc.Ext_literate_haskell + (Pandoc.readerExtensions Pandoc.def) + } + + +-------------------------------------------------------------------------------- +pandocToPresentation + :: FilePath -> PresentationSettings -> Pandoc.Pandoc + -> Either String Presentation +pandocToPresentation pFilePath pSettings pandoc@(Pandoc.Pandoc meta _) = do + let !pTitle = Pandoc.docTitle meta + !pSlides = pandocToSlides pSettings pandoc + !pActiveFragment = (0, 0) + !pAuthor = concat (Pandoc.docAuthors meta) + return Presentation {..} + + +-------------------------------------------------------------------------------- +-- | This re-parses the pandoc metadata block using the YAML library. This +-- avoids the problems caused by pandoc involving rendering Markdown. This +-- should only be used for settings though, not things like title / authors +-- since those /can/ contain markdown. +parseMetadataBlock :: String -> Maybe A.Value +parseMetadataBlock src = do + block <- mbBlock + Yaml.decode $! T.encodeUtf8 $! T.pack block + where + mbBlock = case lines src of + ("---" : ls) -> case break (`elem` ["---", "..."]) ls of + (_, []) -> Nothing + (block, (_ : _)) -> Just (unlines block) + _ -> Nothing + + +-------------------------------------------------------------------------------- +-- | Read settings from the metadata block in the Pandoc document. +readMetaSettings :: String -> Either String PresentationSettings +readMetaSettings src = fromMaybe (Right mempty) $ do + A.Object obj <- parseMetadataBlock src + val <- HMS.lookup "patat" obj + return $! resultToEither $! A.fromJSON val + where + resultToEither :: A.Result a -> Either String a + resultToEither (A.Success x) = Right x + resultToEither (A.Error e) = Left $! + "Error parsing patat settings from metadata: " ++ e + + +-------------------------------------------------------------------------------- +-- | Read settings from "$HOME/.patat.yaml". +readHomeSettings :: IO (Either String PresentationSettings) +readHomeSettings = do + home <- getHomeDirectory + let path = home ".patat.yaml" + exists <- doesFileExist path + if not exists + then return (Right mempty) + else do + contents <- B.readFile path + return $! Yaml.decodeEither contents + + +-------------------------------------------------------------------------------- +pandocToSlides :: PresentationSettings -> Pandoc.Pandoc -> [Slide] +pandocToSlides settings pandoc = + let blockss = splitSlides pandoc in + map (Slide . map Fragment . (fragmentBlocks fragmentSettings)) blockss + where + fragmentSettings = FragmentSettings + { fsIncrementalLists = fromMaybe False (psIncrementalLists settings) + } + + +-------------------------------------------------------------------------------- +-- | Split a pandoc document into slides. If the document contains horizonal +-- rules, we use those as slide delimiters. If there are no horizontal rules, +-- we split using h1 headers. +splitSlides :: Pandoc.Pandoc -> [[Pandoc.Block]] +splitSlides (Pandoc.Pandoc _meta blocks0) + | any (== Pandoc.HorizontalRule) blocks0 = splitAtRules blocks0 + | otherwise = splitAtH1s blocks0 + where + splitAtRules blocks = case break (== Pandoc.HorizontalRule) blocks of + (xs, []) -> [xs] + (xs, (_rule : ys)) -> xs : splitAtRules ys + + splitAtH1s [] = [] + splitAtH1s (b : bs) = case break isH1 bs of + (xs, []) -> [(b : xs)] + (xs, (y : ys)) -> (b : xs) : splitAtH1s (y : ys) + + isH1 (Pandoc.Header i _ _) = i == 1 + isH1 _ = False diff --git a/src/Patat/PrettyPrint.hs b/src/Patat/PrettyPrint.hs new file mode 100644 index 0000000..7b24b37 --- /dev/null +++ b/src/Patat/PrettyPrint.hs @@ -0,0 +1,404 @@ +-------------------------------------------------------------------------------- +-- | This is a small pretty-printing library. +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.PrettyPrint + ( Doc + , toString + , dimensions + , null + + , hPutDoc + , putDoc + + , string + , text + , space + , softline + , hardline + + , wrapAt + + , Trimmable (..) + , indent + + , ansi + + , (<+>) + , (<$$>) + , vcat + + -- * Exotic combinators + , Alignment (..) + , align + , paste + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Reader (asks, local) +import Control.Monad.RWS (RWS, runRWS) +import Control.Monad.State (get, gets, modify) +import Control.Monad.Writer (tell) +import Data.Foldable (Foldable) +import qualified Data.List as L +import Data.Monoid (Monoid, mconcat, mempty, (<>)) +import Data.String (IsString (..)) +import qualified Data.Text as T +import Data.Traversable (Traversable, traverse) +import qualified System.Console.ANSI as Ansi +import qualified System.IO as IO +import Prelude hiding (null) + + +-------------------------------------------------------------------------------- +-- | A simple chunk of text. All ANSI codes are "reset" after printing. +data Chunk + = StringChunk [Ansi.SGR] String + | NewlineChunk + deriving (Eq) + + +-------------------------------------------------------------------------------- +type Chunks = [Chunk] + + +-------------------------------------------------------------------------------- +hPutChunk :: IO.Handle -> Chunk -> IO () +hPutChunk h NewlineChunk = IO.hPutStrLn h "" +hPutChunk h (StringChunk codes str) = do + Ansi.hSetSGR h (reverse codes) + IO.hPutStr h str + Ansi.hSetSGR h [Ansi.Reset] + + +-------------------------------------------------------------------------------- +chunkToString :: Chunk -> String +chunkToString NewlineChunk = "\n" +chunkToString (StringChunk _ str) = str + + +-------------------------------------------------------------------------------- +-- | If two neighboring chunks have the same set of ANSI codes, we can group +-- them together. +optimizeChunks :: Chunks -> Chunks +optimizeChunks (StringChunk c1 s1 : StringChunk c2 s2 : chunks) + | c1 == c2 = optimizeChunks (StringChunk c1 (s1 <> s2) : chunks) + | otherwise = + StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : chunks) +optimizeChunks (x : chunks) = x : optimizeChunks chunks +optimizeChunks [] = [] + + +-------------------------------------------------------------------------------- +chunkLines :: Chunks -> [Chunks] +chunkLines chunks = case break (== NewlineChunk) chunks of + (xs, _newline : ys) -> xs : chunkLines ys + (xs, []) -> [xs] + + +-------------------------------------------------------------------------------- +data DocE + = String String + | Softspace + | Hardspace + | Softline + | Hardline + | WrapAt + { wrapAtCol :: Maybe Int + , wrapDoc :: Doc + } + | Ansi + { ansiCode :: [Ansi.SGR] -> [Ansi.SGR] -- ^ Modifies current codes. + , ansiDoc :: Doc + } + | Indent + { indentFirstLine :: LineBuffer + , indentOtherLines :: LineBuffer + , indentDoc :: Doc + } + + +-------------------------------------------------------------------------------- +chunkToDocE :: Chunk -> DocE +chunkToDocE NewlineChunk = Hardline +chunkToDocE (StringChunk codes str) = Ansi (\_ -> codes) (Doc [String str]) + + +-------------------------------------------------------------------------------- +newtype Doc = Doc {unDoc :: [DocE]} + deriving (Monoid) + + +-------------------------------------------------------------------------------- +instance IsString Doc where + fromString = string + + +-------------------------------------------------------------------------------- +instance Show Doc where + show = toString + + +-------------------------------------------------------------------------------- +data DocEnv = DocEnv + { deCodes :: [Ansi.SGR] -- ^ Most recent ones first in the list + , deIndent :: LineBuffer -- ^ Don't need to store first-line indent + , deWrap :: Maybe Int -- ^ Wrap at columns + } + + +-------------------------------------------------------------------------------- +type DocM = RWS DocEnv Chunks LineBuffer + + +-------------------------------------------------------------------------------- +data Trimmable a + = NotTrimmable !a + | Trimmable !a + deriving (Foldable, Functor, Traversable) + + +-------------------------------------------------------------------------------- +-- | Note that this is reversed so we have fast append +type LineBuffer = [Trimmable Chunk] + + +-------------------------------------------------------------------------------- +bufferToChunks :: LineBuffer -> Chunks +bufferToChunks = map trimmableToChunk . reverse . dropWhile isTrimmable + where + isTrimmable (NotTrimmable _) = False + isTrimmable (Trimmable _) = True + + trimmableToChunk (NotTrimmable c) = c + trimmableToChunk (Trimmable c) = c + + +-------------------------------------------------------------------------------- +docToChunks :: Doc -> Chunks +docToChunks doc0 = + let env0 = DocEnv [] [] Nothing + ((), b, cs) = runRWS (go $ unDoc doc0) env0 mempty in + optimizeChunks (cs <> bufferToChunks b) + where + go :: [DocE] -> DocM () + + go [] = return () + + go (String str : docs) = do + chunk <- makeChunk str + modify (NotTrimmable chunk :) + go docs + + go (Softspace : docs) = do + hard <- softConversion Softspace docs + go (hard : docs) + + go (Hardspace : docs) = do + chunk <- makeChunk " " + modify (NotTrimmable chunk :) + go docs + + go (Softline : docs) = do + hard <- softConversion Softline docs + go (hard : docs) + + go (Hardline : docs) = do + buffer <- get + tell $ bufferToChunks buffer <> [NewlineChunk] + indentation <- asks deIndent + modify $ \_ -> if L.null docs then [] else indentation + go docs + + go (WrapAt {..} : docs) = do + local (\env -> env {deWrap = wrapAtCol}) $ go (unDoc wrapDoc) + go docs + + go (Ansi {..} : docs) = do + local (\env -> env {deCodes = ansiCode (deCodes env)}) $ + go (unDoc ansiDoc) + go docs + + go (Indent {..} : docs) = do + local (\env -> env {deIndent = indentOtherLines ++ deIndent env}) $ do + modify (indentFirstLine ++) + go (unDoc indentDoc) + go docs + + makeChunk :: String -> DocM Chunk + makeChunk str = do + codes <- asks deCodes + return $ StringChunk codes str + + -- Convert 'Softspace' or 'Softline' to 'Hardspace' or 'Hardline' + softConversion :: DocE -> [DocE] -> DocM DocE + softConversion soft docs = do + mbWrapCol <- asks deWrap + case mbWrapCol of + Nothing -> return hard + Just maxCol -> do + -- Slow. + currentLine <- gets (concatMap chunkToString . bufferToChunks) + let currentCol = length currentLine + case nextWordLength docs of + Nothing -> return hard + Just l + | currentCol + 1 + l <= maxCol -> return Hardspace + | otherwise -> return Hardline + where + hard = case soft of + Softspace -> Hardspace + Softline -> Hardline + _ -> soft + + nextWordLength :: [DocE] -> Maybe Int + nextWordLength [] = Nothing + nextWordLength (String x : xs) + | L.null x = nextWordLength xs + | otherwise = Just (length x) + nextWordLength (Softspace : xs) = nextWordLength xs + nextWordLength (Hardspace : xs) = nextWordLength xs + nextWordLength (Softline : xs) = nextWordLength xs + nextWordLength (Hardline : _) = Nothing + nextWordLength (WrapAt {..} : xs) = nextWordLength (unDoc wrapDoc ++ xs) + nextWordLength (Ansi {..} : xs) = nextWordLength (unDoc ansiDoc ++ xs) + nextWordLength (Indent {..} : xs) = nextWordLength (unDoc indentDoc ++ xs) + + +-------------------------------------------------------------------------------- +toString :: Doc -> String +toString = concat . map chunkToString . docToChunks + + +-------------------------------------------------------------------------------- +-- | Returns the rows and columns necessary to render this document +dimensions :: Doc -> (Int, Int) +dimensions doc = + let ls = lines (toString doc) in + (length ls, foldr max 0 (map length ls)) + + +-------------------------------------------------------------------------------- +null :: Doc -> Bool +null doc = case unDoc doc of [] -> True; _ -> False + + +-------------------------------------------------------------------------------- +hPutDoc :: IO.Handle -> Doc -> IO () +hPutDoc h = mapM_ (hPutChunk h) . docToChunks + + +-------------------------------------------------------------------------------- +putDoc :: Doc -> IO () +putDoc = hPutDoc IO.stdout + + +-------------------------------------------------------------------------------- +mkDoc :: DocE -> Doc +mkDoc e = Doc [e] + + +-------------------------------------------------------------------------------- +string :: String -> Doc +string = mkDoc . String -- TODO (jaspervdj): Newline conversion + + +-------------------------------------------------------------------------------- +text :: T.Text -> Doc +text = string . T.unpack + + +-------------------------------------------------------------------------------- +space :: Doc +space = mkDoc Softspace + + +-------------------------------------------------------------------------------- +softline :: Doc +softline = mkDoc Softline + + +-------------------------------------------------------------------------------- +hardline :: Doc +hardline = mkDoc Hardline + + +-------------------------------------------------------------------------------- +wrapAt :: Maybe Int -> Doc -> Doc +wrapAt wrapAtCol wrapDoc = mkDoc WrapAt {..} + + +-------------------------------------------------------------------------------- +indent :: Trimmable Doc -> Trimmable Doc -> Doc -> Doc +indent firstLineDoc otherLinesDoc doc = mkDoc $ Indent + { indentFirstLine = traverse docToChunks firstLineDoc + , indentOtherLines = traverse docToChunks otherLinesDoc + , indentDoc = doc + } + + +-------------------------------------------------------------------------------- +ansi :: [Ansi.SGR] -> Doc -> Doc +ansi codes = mkDoc . Ansi (codes ++) + + +-------------------------------------------------------------------------------- +(<+>) :: Doc -> Doc -> Doc +x <+> y = x <> space <> y +infixr 6 <+> + + +-------------------------------------------------------------------------------- +(<$$>) :: Doc -> Doc -> Doc +x <$$> y = x <> hardline <> y +infixr 5 <$$> + + +-------------------------------------------------------------------------------- +vcat :: [Doc] -> Doc +vcat = mconcat . L.intersperse hardline + + +-------------------------------------------------------------------------------- +data Alignment = AlignLeft | AlignCenter | AlignRight deriving (Eq, Ord, Show) + + +-------------------------------------------------------------------------------- +align :: Int -> Alignment -> Doc -> Doc +align width alignment doc0 = + let chunks0 = docToChunks doc0 + lines_ = chunkLines chunks0 in + vcat + [ Doc (map chunkToDocE (alignLine line)) + | line <- lines_ + ] + where + lineWidth :: [Chunk] -> Int + lineWidth = sum . map (length . chunkToString) + + alignLine :: [Chunk] -> [Chunk] + alignLine line = + let actual = lineWidth line + spaces n = [StringChunk [] (replicate n ' ')] in + case alignment of + AlignLeft -> line <> spaces (width - actual) + AlignRight -> spaces (width - actual) <> line + AlignCenter -> + let r = (width - actual) `div` 2 + l = (width - actual) - r in + spaces l <> line <> spaces r + + +-------------------------------------------------------------------------------- +-- | Like the unix program 'paste'. +paste :: [Doc] -> Doc +paste docs0 = + let chunkss = map docToChunks docs0 :: [Chunks] + cols = map chunkLines chunkss :: [[Chunks]] + rows0 = L.transpose cols :: [[Chunks]] + rows1 = map (map (Doc . map chunkToDocE)) rows0 :: [[Doc]] in + vcat $ map mconcat rows1 diff --git a/src/Patat/Theme.hs b/src/Patat/Theme.hs new file mode 100644 index 0000000..706f825 --- /dev/null +++ b/src/Patat/Theme.hs @@ -0,0 +1,286 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +module Patat.Theme + ( Theme (..) + , defaultTheme + + , Style (..) + + , SyntaxHighlighting (..) + , defaultSyntaxHighlighting + , syntaxHighlight + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (forM_, mplus) +import qualified Data.Aeson as A +import qualified Data.Aeson.TH.Extended as A +import Data.Char (toLower, toUpper) +import Data.List (intercalate, isSuffixOf) +import qualified Data.Map as M +import Data.Maybe (mapMaybe, maybeToList) +import Data.Monoid (Monoid (..), (<>)) +import qualified Data.Text as T +import qualified System.Console.ANSI as Ansi +import qualified Text.Highlighting.Kate as Kate +import Text.Read (readMaybe) +import Prelude + + +-------------------------------------------------------------------------------- +data Theme = Theme + { themeBorders :: !(Maybe Style) + , themeHeader :: !(Maybe Style) + , themeCodeBlock :: !(Maybe Style) + , themeBulletList :: !(Maybe Style) + , themeBulletListMarkers :: !(Maybe T.Text) + , themeOrderedList :: !(Maybe Style) + , themeBlockQuote :: !(Maybe Style) + , themeDefinitionTerm :: !(Maybe Style) + , themeDefinitionList :: !(Maybe Style) + , themeTableHeader :: !(Maybe Style) + , themeTableSeparator :: !(Maybe Style) + , themeLineBlock :: !(Maybe Style) + , themeEmph :: !(Maybe Style) + , themeStrong :: !(Maybe Style) + , themeCode :: !(Maybe Style) + , themeLinkText :: !(Maybe Style) + , themeLinkTarget :: !(Maybe Style) + , themeStrikeout :: !(Maybe Style) + , themeQuoted :: !(Maybe Style) + , themeMath :: !(Maybe Style) + , themeImageText :: !(Maybe Style) + , themeImageTarget :: !(Maybe Style) + , themeSyntaxHighlighting :: !(Maybe SyntaxHighlighting) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Monoid Theme where + mempty = Theme + Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing + + mappend l r = Theme + { themeBorders = mplusOn themeBorders + , themeHeader = mplusOn themeHeader + , themeCodeBlock = mplusOn themeCodeBlock + , themeBulletList = mplusOn themeBulletList + , themeBulletListMarkers = mplusOn themeBulletListMarkers + , themeOrderedList = mplusOn themeOrderedList + , themeBlockQuote = mplusOn themeBlockQuote + , themeDefinitionTerm = mplusOn themeDefinitionTerm + , themeDefinitionList = mplusOn themeDefinitionList + , themeTableHeader = mplusOn themeTableHeader + , themeTableSeparator = mplusOn themeTableSeparator + , themeLineBlock = mplusOn themeLineBlock + , themeEmph = mplusOn themeEmph + , themeStrong = mplusOn themeStrong + , themeCode = mplusOn themeCode + , themeLinkText = mplusOn themeLinkText + , themeLinkTarget = mplusOn themeLinkTarget + , themeStrikeout = mplusOn themeStrikeout + , themeQuoted = mplusOn themeQuoted + , themeMath = mplusOn themeMath + , themeImageText = mplusOn themeImageText + , themeImageTarget = mplusOn themeImageTarget + , themeSyntaxHighlighting = mappendOn themeSyntaxHighlighting + } + where + mplusOn f = f l `mplus` f r + mappendOn f = f l `mappend` f r + + +-------------------------------------------------------------------------------- +defaultTheme :: Theme +defaultTheme = Theme + { themeBorders = dull Ansi.Yellow + , themeHeader = dull Ansi.Blue + , themeCodeBlock = dull Ansi.White <> ondull Ansi.Black + , themeBulletList = dull Ansi.Magenta + , themeBulletListMarkers = Just "-*" + , themeOrderedList = dull Ansi.Magenta + , themeBlockQuote = dull Ansi.Green + , themeDefinitionTerm = dull Ansi.Blue + , themeDefinitionList = dull Ansi.Magenta + , themeTableHeader = dull Ansi.Blue + , themeTableSeparator = dull Ansi.Magenta + , themeLineBlock = dull Ansi.Magenta + , themeEmph = dull Ansi.Green + , themeStrong = dull Ansi.Red <> bold + , themeCode = dull Ansi.White <> ondull Ansi.Black + , themeLinkText = dull Ansi.Green + , themeLinkTarget = dull Ansi.Cyan <> underline + , themeStrikeout = ondull Ansi.Red + , themeQuoted = dull Ansi.Green + , themeMath = dull Ansi.Green + , themeImageText = dull Ansi.Green + , themeImageTarget = dull Ansi.Cyan <> underline + , themeSyntaxHighlighting = Just defaultSyntaxHighlighting + } + where + dull c = Just $ Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c] + ondull c = Just $ Style [Ansi.SetColor Ansi.Background Ansi.Dull c] + bold = Just $ Style [Ansi.SetConsoleIntensity Ansi.BoldIntensity] + underline = Just $ Style [Ansi.SetUnderlining Ansi.SingleUnderline] + + +-------------------------------------------------------------------------------- +newtype Style = Style {unStyle :: [Ansi.SGR]} + deriving (Monoid, Show) + + +-------------------------------------------------------------------------------- +instance A.ToJSON Style where + toJSON = A.toJSON . mapMaybe nameForSGR . unStyle + + +-------------------------------------------------------------------------------- +instance A.FromJSON Style where + parseJSON val = do + names <- A.parseJSON val + sgrs <- mapM toSgr names + return $! Style sgrs + where + toSgr name = case M.lookup name sgrsByName of + Just sgr -> return sgr + Nothing -> fail $! + "Unknown style: " ++ show name ++ ". Known styles are: " ++ + intercalate ", " (map show $ M.keys sgrsByName) + + +-------------------------------------------------------------------------------- +nameForSGR :: Ansi.SGR -> Maybe String +nameForSGR (Ansi.SetColor layer intensity color) = Just $ + (\str -> case layer of + Ansi.Foreground -> str + Ansi.Background -> "on" ++ capitalize str) $ + (case intensity of + Ansi.Dull -> "dull" + Ansi.Vivid -> "vivid") ++ + (case color of + Ansi.Black -> "Black" + Ansi.Red -> "Red" + Ansi.Green -> "Green" + Ansi.Yellow -> "Yellow" + Ansi.Blue -> "Blue" + Ansi.Magenta -> "Magenta" + Ansi.Cyan -> "Cyan" + Ansi.White -> "White") + +nameForSGR (Ansi.SetUnderlining Ansi.SingleUnderline) = Just "underline" + +nameForSGR (Ansi.SetConsoleIntensity Ansi.BoldIntensity) = Just "bold" + +nameForSGR _ = Nothing + + +-------------------------------------------------------------------------------- +sgrsByName :: M.Map String Ansi.SGR +sgrsByName = M.fromList + [ (name, sgr) + | sgr <- knownSgrs + , name <- maybeToList (nameForSGR sgr) + ] + where + -- | It doesn't really matter if we generate "too much" SGRs here since + -- 'nameForSGR' will only pick the ones we support. + knownSgrs = + [ Ansi.SetColor l i c + | l <- [minBound .. maxBound] + , i <- [minBound .. maxBound] + , c <- [minBound .. maxBound] + ] ++ + [Ansi.SetUnderlining u | u <- [minBound .. maxBound]] ++ + [Ansi.SetConsoleIntensity c | c <- [minBound .. maxBound]] + + +-------------------------------------------------------------------------------- +newtype SyntaxHighlighting = SyntaxHighlighting + { unSyntaxHighlighting :: M.Map String Style + } deriving (Monoid, Show, A.ToJSON) + + +-------------------------------------------------------------------------------- +instance A.FromJSON SyntaxHighlighting where + parseJSON val = do + styleMap <- A.parseJSON val + forM_ (M.keys styleMap) $ \k -> case nameToTokenType k of + Just _ -> return () + Nothing -> fail $ "Unknown token type: " ++ show k + return (SyntaxHighlighting styleMap) + + +-------------------------------------------------------------------------------- +defaultSyntaxHighlighting :: SyntaxHighlighting +defaultSyntaxHighlighting = mkSyntaxHighlighting + [ (Kate.KeywordTok, dull Ansi.Yellow) + , (Kate.ControlFlowTok, dull Ansi.Yellow) + + , (Kate.DataTypeTok, dull Ansi.Green) + + , (Kate.DecValTok, dull Ansi.Red) + , (Kate.BaseNTok, dull Ansi.Red) + , (Kate.FloatTok, dull Ansi.Red) + , (Kate.ConstantTok, dull Ansi.Red) + , (Kate.CharTok, dull Ansi.Red) + , (Kate.SpecialCharTok, dull Ansi.Red) + , (Kate.StringTok, dull Ansi.Red) + , (Kate.VerbatimStringTok, dull Ansi.Red) + , (Kate.SpecialStringTok, dull Ansi.Red) + + , (Kate.CommentTok, dull Ansi.Blue) + , (Kate.DocumentationTok, dull Ansi.Blue) + , (Kate.AnnotationTok, dull Ansi.Blue) + , (Kate.CommentVarTok, dull Ansi.Blue) + + , (Kate.ImportTok, dull Ansi.Cyan) + , (Kate.OperatorTok, dull Ansi.Cyan) + , (Kate.FunctionTok, dull Ansi.Cyan) + , (Kate.PreprocessorTok, dull Ansi.Cyan) + ] + where + dull c = Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c] + + mkSyntaxHighlighting ls = SyntaxHighlighting $ + M.fromList [(nameForTokenType tt, s) | (tt, s) <- ls] + + +-------------------------------------------------------------------------------- +nameForTokenType :: Kate.TokenType -> String +nameForTokenType = + unCapitalize . dropTok . show + where + unCapitalize (x : xs) = toLower x : xs + unCapitalize xs = xs + + dropTok :: String -> String + dropTok str + | "Tok" `isSuffixOf` str = take (length str - 3) str + | otherwise = str + + +-------------------------------------------------------------------------------- +nameToTokenType :: String -> Maybe Kate.TokenType +nameToTokenType = readMaybe . capitalize . (++ "Tok") + + +-------------------------------------------------------------------------------- +capitalize :: String -> String +capitalize "" = "" +capitalize (x : xs) = toUpper x : xs + + +-------------------------------------------------------------------------------- +syntaxHighlight :: Theme -> Kate.TokenType -> Maybe Style +syntaxHighlight theme tokenType = do + sh <- themeSyntaxHighlighting theme + M.lookup (nameForTokenType tokenType) (unSyntaxHighlighting sh) + + +-------------------------------------------------------------------------------- +$(A.deriveJSON A.dropPrefixOptions ''Theme) diff --git a/src/Text/Pandoc/Extended.hs b/src/Text/Pandoc/Extended.hs new file mode 100644 index 0000000..941d716 --- /dev/null +++ b/src/Text/Pandoc/Extended.hs @@ -0,0 +1,30 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE LambdaCase #-} +module Text.Pandoc.Extended + ( module Text.Pandoc + + , plainToPara + , newlineToSpace + ) where + + +-------------------------------------------------------------------------------- +import Data.Data.Extended (grecT) +import Text.Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +plainToPara :: [Block] -> [Block] +plainToPara = map $ \case + Plain inlines -> Para inlines + block -> block + + +-------------------------------------------------------------------------------- +newlineToSpace :: [Inline] -> [Inline] +newlineToSpace = grecT $ \case + SoftBreak -> Space + LineBreak -> Space + inline -> inline diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..e3c2c1e --- /dev/null +++ b/stack.yaml @@ -0,0 +1,6 @@ +resolver: lts-7.0 +packages: +- '.' +extra-deps: [] +flags: {} +extra-package-dbs: [] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..9f8e48d --- /dev/null +++ b/test.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -o nounset -o errexit -o pipefail + +srcs=$(find tests -type f ! -name '*.dump') +stuff_went_wrong=false + +for src in $srcs; do + expected="$src.dump" + echo -n "Testing $src... " + actual=$(mktemp) + patat --dump --force "$src" >"$actual" + + if [[ $@ == "--fix" ]]; then + cp "$actual" "$expected" + echo 'Fixed' + elif [[ ! -f "$expected" ]]; then + echo "missing file: $expected" + stuff_went_wrong=true + elif [[ "$(cat "$expected")" == "$(cat "$actual")" ]]; then + echo 'OK' + else + echo 'files differ' + diff "$actual" "$expected" || true + stuff_went_wrong=true + fi +done + +if [[ "$stuff_went_wrong" = true ]]; then + exit 1 +fi diff --git a/tests/01.md b/tests/01.md new file mode 100644 index 0000000..2fbdde2 --- /dev/null +++ b/tests/01.md @@ -0,0 +1,14 @@ +--- +title: This is my presentation +author: Jasper Van der Jeugt +... + +# This is a test + +Hello world + +--- + +# This is a second slide + +lololol diff --git a/tests/01.md.dump b/tests/01.md.dump new file mode 100644 index 0000000..1ae41da --- /dev/null +++ b/tests/01.md.dump @@ -0,0 +1,8 @@ +# This is a test + +Hello world + +---------- +# This is a second slide + +lololol diff --git a/tests/02.lhs b/tests/02.lhs new file mode 100644 index 0000000..e61c2d0 --- /dev/null +++ b/tests/02.lhs @@ -0,0 +1,6 @@ +This is how to do _Hello World_ in Haskell: + +> main :: IO () +> main = putStrLn "Hello World!" + +Cool, right? diff --git a/tests/02.lhs.dump b/tests/02.lhs.dump new file mode 100644 index 0000000..594c1bd --- /dev/null +++ b/tests/02.lhs.dump @@ -0,0 +1,8 @@ +This is how to do Hello World in Haskell: + +   +  main :: IO ()  +  main = putStrLn "Hello World!"  +   + +Cool, right? diff --git a/tests/03.md b/tests/03.md new file mode 100644 index 0000000..6b3ae16 --- /dev/null +++ b/tests/03.md @@ -0,0 +1,46 @@ +Inline markups: + +- ~~striked out~~ +- + +--- + +> Some quote + +> Quote with embedded list: +> +> - Hello +> - World + +--- + +- List with an embedded quote: + + > Tu quoque + + Wow rad stuff. + +- Second item in that list. + +--- + +Code with empty line: + + puts "wow" + + puts "amaze" + +--- + +Code in ordered list: + +1. Do you know the coolest codes? + + It's this: + + fire_missiles() + cancel() + + Great + +2. Also `fib` is pretty cool yeah diff --git a/tests/03.md.dump b/tests/03.md.dump new file mode 100644 index 0000000..e8b6b69 --- /dev/null +++ b/tests/03.md.dump @@ -0,0 +1,48 @@ +Inline markups: + + - ~~striked out~~ + - <http://example.com> + +---------- +> Some quote + +> Quote with embedded list: +>  +>  - Hello +>  - World + +---------- + - List with an embedded quote: + + > Tu quoque + + Wow rad stuff. + + - Second item in that list. + + +---------- +Code with empty line: + +   +  puts "wow"  +   +  puts "amaze"  +   + +---------- +Code in ordered list: + +1. Do you know the coolest codes? + + It's this: + +   +  fire_missiles()  +  cancel()  +   + + Great + +2. Also  fib  is pretty cool yeah + diff --git a/tests/deflist.md b/tests/deflist.md new file mode 100644 index 0000000..81aee19 --- /dev/null +++ b/tests/deflist.md @@ -0,0 +1,20 @@ +Term 1 + +: Definition 1 + +Term 2 with *inline markup* + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +--- + +Term 1 + ~ Definition 1 + +Term 2 + ~ Definition 2a + ~ Definition 2b diff --git a/tests/deflist.md.dump b/tests/deflist.md.dump new file mode 100644 index 0000000..8089fda --- /dev/null +++ b/tests/deflist.md.dump @@ -0,0 +1,24 @@ +Term 1 + +: Definition 1 + +Term 2 with inline markup + +: Definition 2 + +   +  { some code, part of Definition 2 }  +   + + Third paragraph of definition 2. + +---------- +Term 1 + +: Definition 1 + +Term 2 + +: Definition 2a + +: Definition 2b diff --git a/tests/fragments.md b/tests/fragments.md new file mode 100644 index 0000000..510baa2 --- /dev/null +++ b/tests/fragments.md @@ -0,0 +1,27 @@ +--- +patat: + incrementalLists: true +... + +- This list +- is displayed + + * item + * by item + +- Or sometimes + + > * all at + > * once + +--- + +Legen + +. . . + +wait for it + +. . . + +Dary! diff --git a/tests/fragments.md.dump b/tests/fragments.md.dump new file mode 100644 index 0000000..65b7aec --- /dev/null +++ b/tests/fragments.md.dump @@ -0,0 +1,54 @@ + + +~~~~~~~~~~ + - This list + +~~~~~~~~~~ + - This list + - is displayed + + + + +~~~~~~~~~~ + - This list + - is displayed + +  * item + + +~~~~~~~~~~ + - This list + - is displayed + +  * item +  * by item + + +~~~~~~~~~~ + - This list + - is displayed + +  * item +  * by item + + - Or sometimes + +  * all at +  * once + + +---------- +Legen + +~~~~~~~~~~ +Legen + +wait for it + +~~~~~~~~~~ +Legen + +wait for it + +Dary! diff --git a/tests/links.md b/tests/links.md new file mode 100644 index 0000000..153f959 --- /dev/null +++ b/tests/links.md @@ -0,0 +1,8 @@ +This is an "automatic link": . + +This is an [inline link](/url), and here's [one with +a title](http://fsf.org "click here for a good time!"). + +Let's talk about [foo][foosite] + +[foosite]: http://foo.com/ diff --git a/tests/links.md.dump b/tests/links.md.dump new file mode 100644 index 0000000..2862e9a --- /dev/null +++ b/tests/links.md.dump @@ -0,0 +1,10 @@ +This is an "automatic link": <https://jaspervdj.be>. + +This is an [inline link], and here's [one with +a title]. + +Let's talk about [foo] + +[inline link](/url) +[one with a title](http://fsf.org "click here for a good time!") +[foo](http://foo.com/) \ No newline at end of file diff --git a/tests/lists.md b/tests/lists.md new file mode 100644 index 0000000..d534704 --- /dev/null +++ b/tests/lists.md @@ -0,0 +1,13 @@ +- This is a nested list. + + * The nested items should have different list markers. + + * I mean, they can be the same, but it doesn't look nice. + + printf("Nested code block!\n") + + * Cool right? + + Definitely super cool + +- One final item diff --git a/tests/lists.md.dump b/tests/lists.md.dump new file mode 100644 index 0000000..1305289 --- /dev/null +++ b/tests/lists.md.dump @@ -0,0 +1,15 @@ + - This is a nested list. + +  * The nested items should have different list markers. + +  * I mean, they can be the same, but it doesn't look nice. + + printf("Nested code block!\n") + +  * Cool right? + + Definitely super cool + + + - One final item + diff --git a/tests/meta.md b/tests/meta.md new file mode 100644 index 0000000..2ba5db9 --- /dev/null +++ b/tests/meta.md @@ -0,0 +1,12 @@ +--- +patat: + theme: + bulletListMarkers: '<>' +... + +- Hello +- World + * How + * Are + * You + * Doing diff --git a/tests/meta.md.dump b/tests/meta.md.dump new file mode 100644 index 0000000..740ed6b --- /dev/null +++ b/tests/meta.md.dump @@ -0,0 +1,7 @@ + < Hello + < World +  > How +  > Are +  > You +  > Doing + diff --git a/tests/syntax.md b/tests/syntax.md new file mode 100644 index 0000000..f6c803d --- /dev/null +++ b/tests/syntax.md @@ -0,0 +1,14 @@ +--- +patat: + theme: + syntaxHighlighting: + decVal: [bold, onDullRed] +... + +Some simple code: + +```c +int main(int argc, char **argv) { + return 0; +} +``` diff --git a/tests/syntax.md.dump b/tests/syntax.md.dump new file mode 100644 index 0000000..eb4893f --- /dev/null +++ b/tests/syntax.md.dump @@ -0,0 +1,7 @@ +Some simple code: + +   +  int main(int argc, char **argv) {  +  return 0;  +  }  +   diff --git a/tests/tables.md b/tests/tables.md new file mode 100644 index 0000000..fe7d72e --- /dev/null +++ b/tests/tables.md @@ -0,0 +1,48 @@ +# Normal simple table + + Right Left Center Default +------- ------ ---------- ------- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1 + +Table: Demonstration of simple table syntax. + + +# Headerless table + +------- ------ ---------- ------- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1 +------- ------ ---------- ------- + +# Multiline + +------------------------------------------------------------- + Centered Default Right Left + Header Aligned Aligned Aligned +----------- ------- --------------- ------------------------- + First row 12.0 Example of a row that + spans multiple lines. + + Second row 5.0 Here's another one. Note + the blank line between + rows. +------------------------------------------------------------- + +Table: Here's the caption. It, too, may span +multiple lines. + +# Headerless multiline + +----------- ------- --------------- ------------------------- + First row 12.0 Example of a row that + spans multiple lines. + + Second row 5.0 Here's another one. Note + the blank line between + rows. +----------- ------- --------------- ------------------------- + +: Here's a multiline table without headers. diff --git a/tests/tables.md.dump b/tests/tables.md.dump new file mode 100644 index 0000000..0b0a93f --- /dev/null +++ b/tests/tables.md.dump @@ -0,0 +1,48 @@ +# Normal simple table + + Right Left Center Default + ----- ---- ------ ------- + 12 12 12 12  + 123 123 123 123  + 1 1 1 1  + + Table: Demonstration of simple table syntax. + +---------- +# Headerless table + + --- --- --- --- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1  + --- --- --- --- + +---------- +# Multiline + + Centered Default Right Left  + Header Aligned Aligned Aligned  + -------- ------- ------- ------------------------ + First row 12.0 Example of a row that  + spans multiple lines.  +  + Second row 5.0 Here's another one. Note + the blank line between  + rows.  + + Table: Here's the caption. It, too, may span + multiple lines. + +---------- +# Headerless multiline + + ------ --- ---- ------------------------ + First row 12.0 Example of a row that  + spans multiple lines.  +  + Second row 5.0 Here's another one. Note + the blank line between  + rows.  + ------ --- ---- ------------------------ + + Table: Here's a multiline table without headers. diff --git a/tests/themes.md b/tests/themes.md new file mode 100644 index 0000000..6591ece --- /dev/null +++ b/tests/themes.md @@ -0,0 +1,11 @@ +--- +patat: + theme: + bulletListMarkers: '-+' + emph: [onVividRed, underline] +... + +- This is a simple list. + * With _nested_ items. + * One or two. +- The list theming is customized a bit. diff --git a/tests/themes.md.dump b/tests/themes.md.dump new file mode 100644 index 0000000..988214f --- /dev/null +++ b/tests/themes.md.dump @@ -0,0 +1,5 @@ + - This is a simple list. +  + With nested items. +  + One or two. + + - The list theming is customized a bit. diff --git a/tests/wrapping.md b/tests/wrapping.md new file mode 100644 index 0000000..15bc088 --- /dev/null +++ b/tests/wrapping.md @@ -0,0 +1,23 @@ +--- +patat: + wrap: true + columns: 40 +... + +This is a long +sentence over multiple +lines which can be +re-wrapped. + + +This is a super long sentence over a single line which should also be re-wrapped. + + + This is a table and tables should not be wrapped + ------- ------- ---------- ---------- ---------- + 1 2 3 4 5 + 6 7 8 9 10 + +- This is a list +- This list has a really long sentence in it which should also be wrapped with proper indentation +- Another item diff --git a/tests/wrapping.md.dump b/tests/wrapping.md.dump new file mode 100644 index 0000000..e23f9e3 --- /dev/null +++ b/tests/wrapping.md.dump @@ -0,0 +1,17 @@ +This is a long sentence over multiple +lines which can be re-wrapped. + +This is a super long sentence over a +single line which should also be +re-wrapped. + + This is a table and tables should not be wrapped + ------- ------- ---------- ---------- ---------- + 1 2 3 4 5  + 6 7 8 9 10  + + - This is a list + - This list has a really long sentence + in it which should also be wrapped + with proper indentation + - Another item -- 2.30.2